merge updates

This commit is contained in:
Vojtěch Fošnár 2023-02-19 12:32:44 +01:00
commit 6a89beaeb1
No known key found for this signature in database
GPG key ID: 657727E71C40859A
1008 changed files with 89591 additions and 37779 deletions

1
.gitignore vendored
View file

@ -5,3 +5,4 @@
*.got.svg
e2e_report.html
bin
out

View file

@ -225,6 +225,8 @@ let us know and we'll be happy to include it here!
- **Maven plugin**: [https://github.com/andrinmeier/unofficial-d2lang-maven-plugin](https://github.com/andrinmeier/unofficial-d2lang-maven-plugin)
- **Confluence plugin**: [https://github.com/andrinmeier/unofficial-d2lang-confluence-plugin](https://github.com/andrinmeier/unofficial-d2lang-confluence-plugin)
- **CIL (C#, Visual Basic, F#, C++ CLR) to D2**: [https://github.com/HugoVG/AppDiagram](https://github.com/HugoVG/AppDiagram)
- **D2 Snippets (for text editors)**: [https://github.com/Paracelsus-Rose/D2-Language-Code-Snippets](https://github.com/Paracelsus-Rose/D2-Language-Code-Snippets)
- **Mongo to D2**: [https://github.com/novuhq/mongo-to-D2](https://github.com/novuhq/mongo-to-D2)
### Misc

19
ci/cov.sh Executable file
View file

@ -0,0 +1,19 @@
#!/bin/sh
set -eu
cd -- "$(dirname "$0")/.."
. ./ci/sub/lib.sh
main() {
if [ "$*" = "" ]; then
set ./...
fi
mkdir -p out
capcode ./ci/test.sh -covermode=atomic -coverprofile=out/cov.prof "$@"
go tool cover -html=out/cov.prof -o=out/cov.html
go tool cover -func=out/cov.prof | grep '^total:' \
| sed 's#^total:.*(statements)[[:space:]]*\([0-9.%]*\)#TOTAL:\t\1#'
return "$code"
}
main "$@"

View file

@ -1,7 +1,7 @@
#!/bin/sh
set -eu
export REPORT_OUTPUT="out/e2e_report.html"
export REPORT_OUTPUT="./e2etests/out/e2e_report.html"
rm -f $REPORT_OUTPUT
export E2E_REPORT=1
@ -9,7 +9,13 @@ FORCE_COLOR=1 DEBUG=1 go run ./e2etests/report/main.go "$@";
if [ -z "${NO_OPEN:-}" ]; then
if [ -s "$REPORT_OUTPUT" ]; then
open "$REPORT_OUTPUT"
if command -v open >/dev/null; then
open "$REPORT_OUTPUT"
elif command -v xdg-open >/dev/null; then
xdg-open "$REPORT_OUTPUT"
else
echo "Please open $REPORT_OUTPUT"
fi
else
echo "The report is empty"
fi

View file

@ -252,10 +252,10 @@ install_post_standalone() {
Extend your \$PATH to use d2:
export PATH=$PREFIX/bin:\$PATH
Then run:
${TALA+D2_LAYOUT=tala }d2 --help
${TALA:+D2_LAYOUT=tala }d2 --help
EOF
else
log "Run ${TALA+D2_LAYOUT=tala }d2 --help for usage."
log "Run ${TALA:+D2_LAYOUT=tala }d2 --help for usage."
fi
if ! manpath 2>/dev/null | grep -qF "$PREFIX/share/man"; then
logcat >&2 <<EOF
@ -282,7 +282,7 @@ install_post_brew() {
fi
log "Rerun this install script with --uninstall to uninstall."
log
log "Run ${TALA+D2_LAYOUT=tala }d2 --help for usage."
log "Run ${TALA:+D2_LAYOUT=tala }d2 --help for usage."
log "Run man d2 for detailed docs."
if [ -n "${TALA-}" ]; then
log "Run man d2plugin-tala for detailed TALA docs."
@ -485,7 +485,7 @@ fetch_release_info() {
}
curl_gh() {
sh_c curl -fL ${GITHUB_TOKEN+"-H \"Authorization: Bearer \$GITHUB_TOKEN\""} "$@"
sh_c curl -fL ${GITHUB_TOKEN:+"-H \"Authorization: Bearer \$GITHUB_TOKEN\""} "$@"
}
fetch_gh() {

View file

@ -331,6 +331,13 @@ sudo -E apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-
sudo groupadd docker || true
sudo usermod -aG docker \$USER
printf %s '$CI_DOCKER_TOKEN' | docker login -u terrastruct --password-stdin
# For building images cross platform.
sudo -E apt-get install -y qemu qemu-user-static
if docker buildx ls | grep -q 'default \*'; then
docker buildx create --use
fi
mkdir -p \$HOME/.local/bin
mkdir -p \$HOME/.local/share/man
EOF
@ -387,7 +394,7 @@ init_remote_env() {
sh_c ssh "$REMOTE_HOST" "sudo systemctl restart sshd"
# ubuntu has $PATH hard coded in /etc/environment for some reason. It takes precedence
# over ~/.ssh/environment.
sh_c ssh "$REMOTE_HOST" "sudo rm /etc/environment"
sh_c ssh "$REMOTE_HOST" "sudo rm -f /etc/environment"
fi
}

View file

@ -33,11 +33,11 @@ main() {
done
shift "$FLAGSHIFT"
REMOTE_HOST=$CI_HOST_D2_LINUX_AMD64 && runjob linux-amd64 ssh "$REMOTE_HOST" "$@"
REMOTE_HOST=$CI_HOST_D2_LINUX_ARM64 && runjob linux-arm64 ssh "$REMOTE_HOST" "$@"
REMOTE_HOST=$CI_HOST_D2_MACOS_AMD64 && runjob macos-amd64 ssh "$REMOTE_HOST" "$@"
REMOTE_HOST=$CI_HOST_D2_MACOS_ARM64 && runjob macos-arm64 ssh "$REMOTE_HOST" "$@"
REMOTE_HOST=$CI_HOST_D2_WINDOWS_AMD64 && runjob macos-arm64 ssh "$REMOTE_HOST" "$@"
REMOTE_HOST=$CI_D2_LINUX_AMD64 && runjob linux-amd64 ssh "$REMOTE_HOST" "$@"
REMOTE_HOST=$CI_D2_LINUX_ARM64 && runjob linux-arm64 ssh "$REMOTE_HOST" "$@"
REMOTE_HOST=$CI_D2_MACOS_AMD64 && runjob macos-amd64 ssh "$REMOTE_HOST" "$@"
REMOTE_HOST=$CI_D2_MACOS_ARM64 && runjob macos-arm64 ssh "$REMOTE_HOST" "$@"
REMOTE_HOST=$CI_D2_WINDOWS_AMD64 && runjob windows-amd64 ssh "$REMOTE_HOST" "$@"
}
main "$@"

View file

@ -58,6 +58,9 @@ Flags:
images into the daemon for push later. It's not slow though to use --push-docker after
building the image as nearly all artifacts are cached.
Automatically set if called from release.sh
--latest-docker
Mark the built image with the latest tag. Automatically set if called from release.sh
EOF
}
@ -113,6 +116,10 @@ main() {
flag_noarg && shift "$FLAGSHIFT"
PUSH_DOCKER=1
;;
latest-docker)
flag_noarg && shift "$FLAGSHIFT"
LATEST_DOCKER=1
;;
*)
flag_errusage "unrecognized flag $FLAGRAW"
;;
@ -149,7 +156,7 @@ main() {
runjob windows/arm64 'OS=windows ARCH=arm64 build' &
waitjobs
runjob linux/dockerimage 'OS=linux build_docker_image' &
runjob linux/docker build_docker &
runjob windows/amd64/msi 'OS=windows ARCH=amd64 build_windows_msi' &
waitjobs
}
@ -247,14 +254,27 @@ ARCHIVE=$ARCHIVE \
sh_c rsync --archive --human-readable "$REMOTE_HOST:src/d2/$ARCHIVE" "$ARCHIVE"
)}
build_docker_image() {
D2_DOCKER_IMAGE=${D2_DOCKER_IMAGE:-terrastruct/d2}
flags='--load'
if [ -n "${PUSH_DOCKER-}" -o -n "${RELEASE-}" ]; then
flags='--push --platform linux/amd64,linux/arm64'
build_docker() {
if [ -n "${LOCAL-}" ]; then
sh_c ./ci/release/docker/build.sh \
--version="$VERSION" \
${PUSH_DOCKER:+--push} \
${LATEST_DOCKER:+--latest}
return 0
fi
sh_c rsync --archive --human-readable ./ci/release/Dockerfile_entrypoint.sh "./ci/release/build/$VERSION"
sh_c docker buildx build $flags -t "$D2_DOCKER_IMAGE:$VERSION" -t "$D2_DOCKER_IMAGE:latest" --build-arg "VERSION=$VERSION" -f ./ci/release/Dockerfile "./ci/release/build/$VERSION"
sh_c lockfile_ssh "$CI_D2_LINUX_AMD64" .d2-build-lock
sh_c gitsync "$CI_D2_LINUX_AMD64" src/d2
sh_c rsync --archive --human-readable \
"$BUILD_DIR/d2-$VERSION"-linux-*.tar.gz \
"$CI_D2_LINUX_AMD64:src/d2/$BUILD_DIR/"
sh_c ssh "$CI_D2_LINUX_AMD64" \
"D2_DOCKER_IMAGE=${D2_DOCKER_IMAGE-}" \
"RELEASE=${RELEASE-}" \
./src/d2/ci/release/docker/build.sh \
--version="$VERSION" \
${PUSH_DOCKER:+--push} \
${LATEST_DOCKER:+--latest}
}
build_windows_msi() {

View file

@ -1,15 +1,16 @@
#### Features 🚀
- `double-border` keyword implemented. [#565](https://github.com/terrastruct/d2/pull/565)
- The [Dockerfile](./docs/INSTALL.md#docker) now supports rendering PNGs [#594](https://github.com/terrastruct/d2/issues/594)
- There was a minor breaking change as part of this where the default working directory of the Dockerfile is now `/home/debian/src` instead of `/root/src` to allow UID remapping with [`fixuid`](https://github.com/boxboat/fixuid).
- `d2 fmt` accepts multiple files to be formatted [#718](https://github.com/terrastruct/d2/issues/718)
- Many non-Latin languages (e.g. Chinese, Japanese, Korean) are usable now that multi-byte characters are measured correctly. [#817](https://github.com/terrastruct/d2/pull/817)
- Fix duplicate success logs in watch mode. [830](https://github.com/terrastruct/d2/pull/830)
#### Improvements 🧹
- Code snippets use bold and italic font styles as determined by highlighter [#710](https://github.com/terrastruct/d2/issues/710), [#741](https://github.com/terrastruct/d2/issues/741)
- Cleaner watch mode logs without timestamps. [830](https://github.com/terrastruct/d2/pull/830)
#### Bugfixes ⛑️
- Fixes groups overlapping in sequence diagrams when they end in a self loop. [#728](https://github.com/terrastruct/d2/pull/728)
- Fixes edge case where layouts with dagre show a connection from the bottom side of shapes being slightly disconnected from the shape. [#820](https://github.com/terrastruct/d2/pull/820)
- Fixes rare compiler bug when using underscores in edges to create objects across containers. [#824](https://github.com/terrastruct/d2/pull/824)
- Fixes rare possibility of rendered connections being hidden or cut off. [#828](https://github.com/terrastruct/d2/pull/828)
- Creating nested children within `sql_table` and `class` shapes are now prevented (caused confusion when accidentally done). [#834](https://github.com/terrastruct/d2/pull/834)
- Fixes graph deserialization bug. [#837](https://github.com/terrastruct/d2/pull/837)

View file

@ -0,0 +1,56 @@
Here's what a D2 diagram looks like in 0.1 (left) vs 0.2 (right):
![before-after](https://user-images.githubusercontent.com/3120367/218556631-829047e5-e2f7-43e5-b98e-e81b4f76bdb2.jpg)
Much more legible, especially in larger diagrams! This upgrade trims a lot of the excess whitespace present before and makes diagrams more compact. We've also combed through each shape to improve their label and icon positions, paddings, and aspect ratios at different sizes. Example of icons and labels avoiding collisions:
<img width="509" alt="aws icons" src="https://user-images.githubusercontent.com/3120367/218557539-0e9ef284-363c-43d6-bc8d-157768a57aca.png">
We've also put up a hosted icon site for you to conveniently find common software architecture icons to include in your D2 diagrams. [https://icons.terrastruct.com](https://icons.terrastruct.com)
<img width="1380" alt="icons" src="https://user-images.githubusercontent.com/3120367/218560291-a9123142-5840-4fbe-95f7-78b1b539cc23.png">
There's also been a major compiler rewrite. It's fixed many minor compiler bugs, but most importantly, it implements multi-board diagrams. Stay tuned for more as we write docs and make this accessible in the next release!
#### Features 🚀
- `double-border` keyword implemented. [#565](https://github.com/terrastruct/d2/pull/565)
- The [Dockerfile](./docs/INSTALL.md#docker) now supports rendering PNGs [#594](https://github.com/terrastruct/d2/issues/594)
- There was a minor breaking change as part of this where the default working directory of the Dockerfile is now `/home/debian/src` instead of `/root/src` to allow UID remapping with [`fixuid`](https://github.com/boxboat/fixuid).
- `d2 fmt` accepts multiple files to be formatted [#718](https://github.com/terrastruct/d2/issues/718)
- `font-size` works for `sql_table` and `class` shapes [#769](https://github.com/terrastruct/d2/issues/769)
- You can now use the reserved keywords `layers`/`scenarios`/`steps` to define diagrams with multiple levels of abstractions. Coming soon. [#714](https://github.com/terrastruct/d2/pull/714)
#### Improvements 🧹
- Reduces default padding of shapes. [#702](https://github.com/terrastruct/d2/pull/702)
- Ensures labels fit inside shapes with shape-specific inner bounding boxes. [#702](https://github.com/terrastruct/d2/pull/702)
- dagre container labels changed positions to outside the shape. Many previously obscured container labels are now legible. [#788](https://github.com/terrastruct/d2/pull/788)
- Container icons are placed top-left instead of center, to ensure no collisions with children. [#806](https://github.com/terrastruct/d2/pull/806)
- Code snippets use bold and italic font styles as determined by highlighter [#710](https://github.com/terrastruct/d2/issues/710), [#741](https://github.com/terrastruct/d2/issues/741)
- Improves package shape dimensions with short height. [#702](https://github.com/terrastruct/d2/pull/702)
- Sequence diagrams are rendered more compacted, both vertically and horizontally. [#796](https://github.com/terrastruct/d2/pull/796)
- Keeps person shape from becoming too distorted. [#702](https://github.com/terrastruct/d2/pull/702)
- Keeps oval shape from becoming too thin. [#807](https://github.com/terrastruct/d2/pull/807)
- Ensures shapes with icons have enough padding for their labels. [#702](https://github.com/terrastruct/d2/pull/702)
- `--force-appendix` flag adds an appendix to SVG outputs with tooltips or links. [#761](https://github.com/terrastruct/d2/pull/761)
- `d2 themes` subcommand to list themes. [#760](https://github.com/terrastruct/d2/pull/760)
- `sql_table` header left-aligned with column [#769](https://github.com/terrastruct/d2/pull/769)
- Sequence diagram edge group labels are clearer [#782](https://github.com/terrastruct/d2/pull/782)
#### Bugfixes ⛑️
- Fixes groups overlapping in sequence diagrams when they end in a self loop. [#728](https://github.com/terrastruct/d2/pull/728)
- Fixes dimensions of unlabeled squares or circles with only a set width or height. [#702](https://github.com/terrastruct/d2/pull/702)
- Fixes scaling of actor shapes in sequence diagrams. [#702](https://github.com/terrastruct/d2/pull/702)
- Sequence diagram note ordering was sometimes wrong. [#796](https://github.com/terrastruct/d2/pull/796)
- Images can now be set to sizes smaller than 128x128. [#702](https://github.com/terrastruct/d2/pull/702)
- Tooltips with ampersand would result in invalid SVGs. [#798](https://github.com/terrastruct/d2/pull/798)
- Fixes class height when there are no rows. [#756](https://github.com/terrastruct/d2/pull/756)
- Border radius was not firefox-compatible. [#799](https://github.com/terrastruct/d2/pull/799)
#### Breaking changes
- You can no longer use keywords intended for use under `style` outside and vice versa. e.g. `obj.style.shape` and `obj.double-border` are now illegal. The correct usages have always been `obj.shape` and `obj.style.double-border`; it just wasn't enforced until now.

View file

@ -7,7 +7,9 @@ RUN apt-get update && apt-get install -y ca-certificates curl dumb-init sudo
RUN curl -fsSL https://deb.nodesource.com/setup_19.x | bash -s - && \
apt-get install -y nodejs
RUN npx playwright install-deps
# https://github.com/microsoft/playwright/issues/18319
# Hopefully soon.
RUN if [ "$TARGETARCH" = amd64 ]; then npx playwright install-deps; fi
RUN adduser --gecos '' --disabled-password debian \
&& echo "debian ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers.d/nopasswd
@ -19,14 +21,14 @@ RUN curl -fsSL "https://github.com/boxboat/fixuid/releases/download/v0.5/fixuid-
&& printf "user: debian\ngroup: debian\npaths: [/home/debian]\n" > /etc/fixuid/config.yml
COPY ./d2-*-linux-$TARGETARCH.tar.gz /tmp
ADD ./Dockerfile_entrypoint.sh /usr/local/bin/entrypoint.sh
ADD ./entrypoint.sh /usr/local/bin/entrypoint.sh
RUN mkdir -p /usr/local/lib/d2 \
&& tar -C /usr/local/lib/d2 -xzf /tmp/d2-*-linux-"$TARGETARCH".tar.gz \
&& /usr/local/lib/d2/d2-*/scripts/install.sh \
&& rm -Rf /tmp/d2-*-linux-"$TARGETARCH".tar.gz
USER debian:debian
RUN d2 init-playwright
RUN if [ "$TARGETARCH" = amd64 ]; then d2 init-playwright; fi
WORKDIR /home/debian/src
EXPOSE 8080

64
ci/release/docker/build.sh Executable file
View file

@ -0,0 +1,64 @@
#!/bin/sh
set -eu
. "$(dirname "$0")/../../../ci/sub/lib.sh"
cd -- "$(dirname "$0")/../../.."
help() {
cat <<EOF
usage: $0 [-p|--push] [--latest] [--version=str]
EOF
}
main() {
while flag_parse "$@"; do
case "$FLAG" in
h|help)
help
return 0
;;
p|push)
flag_noarg && shift "$FLAGSHIFT"
PUSH=1
;;
latest)
flag_noarg && shift "$FLAGSHIFT"
LATEST=1
;;
version)
flag_reqarg && shift "$FLAGSHIFT"
VERSION=$FLAGARG
;;
*)
flag_errusage "unrecognized flag $FLAGRAW"
;;
esac
done
shift "$FLAGSHIFT"
if [ -z "${VERSION-}" ]; then
VERSION=$(readlink ./ci/release/build/latest)
fi
D2_DOCKER_IMAGE=${D2_DOCKER_IMAGE:-terrastruct/d2}
sh_c mkdir -p "./ci/release/build/$VERSION/docker"
sh_c cp \
"./ci/release/build/$VERSION/d2-$VERSION"-linux-*.tar.gz \
"./ci/release/build/$VERSION/docker/"
sh_c cp \
./ci/release/docker/entrypoint.sh \
"./ci/release/build/$VERSION/docker/entrypoint.sh"
flags='--load'
if [ -n "${PUSH-}" -o -n "${RELEASE-}" ]; then
flags='--push --platform linux/amd64,linux/arm64'
fi
if [ -n "${LATEST-}" -o -n "${RELEASE-}" ]; then
flags="$flags -t $D2_DOCKER_IMAGE:latest"
fi
sh_c docker buildx build $flags \
-t "$D2_DOCKER_IMAGE:$VERSION" \
-f ./ci/release/docker/Dockerfile "./ci/release/build/$VERSION/docker"
}
main "$@"

View file

@ -41,6 +41,15 @@ render anyway to enable iteration on a broken diagram.
.Pp
See more docs, the source code and license at
.Lk https://oss.terrastruct.com/d2
.Ns .
.Pp
Hosted icons at
.Lk https://icons.terrastruct.com
.Ns .
.Pp
Playground runner at
.Lk https://play.d2lang.com
.Ns .
.Sh OPTIONS
.Bl -tag -width Fl
.It Fl w , -watch Ar false
@ -55,8 +64,7 @@ Port listening address when used with
.Ar watch
.Ns .
.It Fl t , -theme Ar 0
Set the diagram theme to the passed integer. For a list of available options, see
.Lk https://oss.terrastruct.com/d2
Set the diagram theme ID
.Ns .
.It Fl s , -sketch Ar false
Renders the diagram to look like it was sketched by hand
@ -70,6 +78,9 @@ Set the diagram layout engine to the passed string. For a list of available opti
.Ns .
.It Fl b , -bundle Ar true
Bundle all assets and layers into the output svg.
.It Fl -force-appendix Ar false
An appendix for tooltips and links is added to PNG exports since they are not interactive. Setting this to true adds an appendix to SVG exports as well
.Ns .
.It Fl d , -debug
Print debug logs.
.It Fl h , -help
@ -83,6 +94,8 @@ Print version information and exit.
Lists available layout engine options with short help.
.It Ar layout Op Ar name
Display long help for a particular layout engine, including its configuration options.
.It Ar themes
Lists available themes.
.It Ar fmt Ar file.d2 ...
Format all passed files.
.El

2
ci/sub

@ -1 +1 @@
Subproject commit 8ac704818b5d7ab519e4b87caf5eb79716493709
Subproject commit 512bad5a958c5e33ba9b3e89dfac1bfd6002f98c

View file

@ -1,3 +1,5 @@
// TODO: Remove boxes and cleanup like d2ir
//
// d2ast implements the d2 language's abstract syntax tree.
//
// Special characters to think about in parser:
@ -149,6 +151,10 @@ func (r *Range) UnmarshalText(b []byte) (err error) {
return r.End.UnmarshalText(end)
}
func (r Range) Before(r2 Range) bool {
return r.Start.Before(r2.Start)
}
// Position represents a line:column and byte position in a file.
//
// note: Line and Column are zero indexed.
@ -257,6 +263,10 @@ func (p Position) SubtractString(s string, byUTF16 bool) Position {
return p
}
func (p Position) Before(p2 Position) bool {
return p.Byte < p2.Byte
}
// MapNode is implemented by nodes that may be children of Maps.
type MapNode interface {
Node
@ -402,7 +412,7 @@ func (s *SingleQuotedString) scalar() {}
func (s *BlockString) scalar() {}
// TODO: mistake, move into parse.go
func (n *Null) ScalarString() string { return n.Type() }
func (n *Null) ScalarString() string { return "" }
func (b *Boolean) ScalarString() string { return strconv.FormatBool(b.Value) }
func (n *Number) ScalarString() string { return n.Raw }
func (s *UnquotedString) ScalarString() string {
@ -648,6 +658,21 @@ type KeyPath struct {
Path []*StringBox `json:"path"`
}
func MakeKeyPath(a []string) *KeyPath {
kp := &KeyPath{}
for _, el := range a {
kp.Path = append(kp.Path, MakeValueBox(RawString(el, true)).StringBox())
}
return kp
}
func (kp *KeyPath) IDA() (ida []string) {
for _, el := range kp.Path {
ida = append(ida, el.Unbox().ScalarString())
}
return ida
}
type Edge struct {
Range Range `json:"range"`
@ -729,6 +754,37 @@ type ArrayNodeBox struct {
Map *Map `json:"map,omitempty"`
}
func MakeArrayNodeBox(an ArrayNode) ArrayNodeBox {
var ab ArrayNodeBox
switch an := an.(type) {
case *Comment:
ab.Comment = an
case *BlockComment:
ab.BlockComment = an
case *Substitution:
ab.Substitution = an
case *Null:
ab.Null = an
case *Boolean:
ab.Boolean = an
case *Number:
ab.Number = an
case *UnquotedString:
ab.UnquotedString = an
case *DoubleQuotedString:
ab.DoubleQuotedString = an
case *SingleQuotedString:
ab.SingleQuotedString = an
case *BlockString:
ab.BlockString = an
case *Array:
ab.Array = an
case *Map:
ab.Map = an
}
return ab
}
func (ab ArrayNodeBox) Unbox() ArrayNode {
switch {
case ab.Comment != nil:

View file

@ -15,13 +15,16 @@ import (
"oss.terrastruct.com/d2/d2target"
)
const complexIDs = false
func GenDSL(maxi int) (_ string, err error) {
gs := &dslGenState{
rand: mathrand.New(mathrand.NewSource(time.Now().UnixNano())),
g: d2graph.NewGraph(&d2ast.Map{}),
g: d2graph.NewGraph(),
nodeShapes: make(map[string]string),
nodeContainer: make(map[string]string),
}
gs.g.AST = &d2ast.Map{}
err = gs.gen(maxi)
if err != nil {
return "", err
@ -61,7 +64,11 @@ func (gs *dslGenState) gen(maxi int) error {
}
func (gs *dslGenState) genNode(containerID string) (string, error) {
nodeID := gs.randStr(32, true)
maxLen := 8
if complexIDs {
maxLen = 32
}
nodeID := gs.randStr(maxLen, true)
if containerID != "" {
nodeID = containerID + "." + nodeID
}
@ -94,7 +101,11 @@ func (gs *dslGenState) node() error {
if gs.roll(25, 75) == 0 {
// 25% chance of adding a label.
gs.g, err = d2oracle.Set(gs.g, nodeID, nil, go2.Pointer(gs.randStr(256, false)))
maxLen := 8
if complexIDs {
maxLen = 256
}
gs.g, err = d2oracle.Set(gs.g, nodeID, nil, go2.Pointer(gs.randStr(maxLen, false)))
if err != nil {
return err
}
@ -153,7 +164,11 @@ func (gs *dslGenState) edge() error {
return err
}
if gs.randBool() {
gs.g, err = d2oracle.Set(gs.g, key, nil, go2.Pointer(gs.randStr(128, false)))
maxLen := 8
if complexIDs {
maxLen = 128
}
gs.g, err = d2oracle.Set(gs.g, key, nil, go2.Pointer(gs.randStr(maxLen, false)))
if err != nil {
return err
}
@ -190,11 +205,15 @@ func (gs *dslGenState) randBool() bool {
// TODO go back to using xrand.String, currently some incompatibility with
// stuffing these strings into a script for dagre
func randRune() rune {
if mathrand.Int31n(100) == 0 {
// Generate newline 1% of the time.
return '\n'
if complexIDs {
if mathrand.Int31n(100) == 0 {
// Generate newline 1% of the time.
return '\n'
}
return mathrand.Int31n(128) + 1
} else {
return mathrand.Int31n(26) + 97
}
return mathrand.Int31n(128) + 1
}
func (gs *dslGenState) findOuterSequenceDiagram(nodeID string) string {

View file

@ -169,6 +169,10 @@ func testPinned(t *testing.T, outDir string) {
name: "orientation",
text: "a: {\n b\n c\n }\n a <- a.c\n a.b -> a\n",
},
{
name: "cannot create edge between boards",
text: `"" <-> ""`,
},
}
for _, tc := range testCases {

File diff suppressed because it is too large Load diff

View file

@ -8,12 +8,13 @@ import (
tassert "github.com/stretchr/testify/assert"
"oss.terrastruct.com/util-go/assert"
"oss.terrastruct.com/util-go/diff"
"oss.terrastruct.com/d2/d2compiler"
"oss.terrastruct.com/d2/d2format"
"oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/d2target"
"oss.terrastruct.com/util-go/assert"
"oss.terrastruct.com/util-go/diff"
)
func TestCompile(t *testing.T) {
@ -85,7 +86,6 @@ x: {
}
},
},
{
name: "dimensions_on_nonimage",
@ -113,6 +113,17 @@ x: {
}
},
},
{
name: "positions",
text: `hey: {
top: 200
left: 230
}
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
tassert.Equal(t, "200", g.Objects[0].Attributes.Top.Value)
},
},
{
name: "equal_dimensions_on_circle",
@ -123,8 +134,7 @@ x: {
}
`,
expErr: `d2/testdata/d2compiler/TestCompile/equal_dimensions_on_circle.d2:3:2: width and height must be equal for circle shapes
d2/testdata/d2compiler/TestCompile/equal_dimensions_on_circle.d2:4:2: width and height must be equal for circle shapes
`,
d2/testdata/d2compiler/TestCompile/equal_dimensions_on_circle.d2:4:2: width and height must be equal for circle shapes`,
},
{
name: "single_dimension_on_circle",
@ -207,8 +217,7 @@ d2/testdata/d2compiler/TestCompile/no_dimensions_on_containers.d2:16:3: height c
d2/testdata/d2compiler/TestCompile/no_dimensions_on_containers.d2:25:3: width cannot be used on container: containers.oval container
d2/testdata/d2compiler/TestCompile/no_dimensions_on_containers.d2:26:3: height cannot be used on container: containers.oval container
d2/testdata/d2compiler/TestCompile/no_dimensions_on_containers.d2:36:3: width cannot be used on container: containers.hexagon container
d2/testdata/d2compiler/TestCompile/no_dimensions_on_containers.d2:37:3: height cannot be used on container: containers.hexagon container
`,
d2/testdata/d2compiler/TestCompile/no_dimensions_on_containers.d2:37:3: height cannot be used on container: containers.hexagon container`,
},
{
name: "dimension_with_style",
@ -241,8 +250,7 @@ d2/testdata/d2compiler/TestCompile/no_dimensions_on_containers.d2:37:3: height c
}
}
`,
expErr: `d2/testdata/d2compiler/TestCompile/shape_unquoted_hex.d2:3:10: missing value after colon
`,
expErr: `d2/testdata/d2compiler/TestCompile/shape_unquoted_hex.d2:3:10: missing value after colon`,
},
{
name: "edge_unquoted_hex",
@ -253,8 +261,7 @@ d2/testdata/d2compiler/TestCompile/no_dimensions_on_containers.d2:37:3: height c
}
}
`,
expErr: `d2/testdata/d2compiler/TestCompile/edge_unquoted_hex.d2:3:10: missing value after colon
`,
expErr: `d2/testdata/d2compiler/TestCompile/edge_unquoted_hex.d2:3:10: missing value after colon`,
},
{
name: "blank_underscore",
@ -264,8 +271,7 @@ d2/testdata/d2compiler/TestCompile/no_dimensions_on_containers.d2:37:3: height c
_
}
`,
expErr: `d2/testdata/d2compiler/TestCompile/blank_underscore.d2:3:3: invalid use of parent "_"
`,
expErr: `d2/testdata/d2compiler/TestCompile/blank_underscore.d2:3:3: field key must contain more than underscores`,
},
{
name: "image_non_style",
@ -276,8 +282,7 @@ d2/testdata/d2compiler/TestCompile/no_dimensions_on_containers.d2:37:3: height c
name: y
}
`,
expErr: `d2/testdata/d2compiler/TestCompile/image_non_style.d2:4:3: image shapes cannot have children.
`,
expErr: `d2/testdata/d2compiler/TestCompile/image_non_style.d2:4:3: image shapes cannot have children.`,
},
{
name: "stroke-width",
@ -302,8 +307,7 @@ d2/testdata/d2compiler/TestCompile/no_dimensions_on_containers.d2:37:3: height c
style.stroke-width: -1
}
`,
expErr: `d2/testdata/d2compiler/TestCompile/illegal-stroke-width.d2:2:23: expected "stroke-width" to be a number between 0 and 15
`,
expErr: `d2/testdata/d2compiler/TestCompile/illegal-stroke-width.d2:2:23: expected "stroke-width" to be a number between 0 and 15`,
},
{
name: "underscore_parent_create",
@ -340,8 +344,18 @@ x: {
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
tassert.Equal(t, "y", g.Objects[1].ID)
tassert.Equal(t, g.Root.AbsID(), g.Objects[1].References[0].ScopeObj.AbsID())
tassert.Equal(t, g.Objects[0].AbsID(), g.Objects[1].References[0].UnresolvedScopeObj.AbsID())
tassert.Equal(t, g.Objects[0].AbsID(), g.Objects[1].References[0].ScopeObj.AbsID())
},
},
{
name: "underscore_connection",
text: `a: {
_.c.d -> _.c.b
}
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
tassert.Equal(t, 4, len(g.Objects))
tassert.Equal(t, 1, len(g.Edges))
},
},
{
@ -456,8 +470,7 @@ x: {
text: `
_.x
`,
expErr: `d2/testdata/d2compiler/TestCompile/underscore_parent_root.d2:2:1: parent "_" cannot be used in the root scope
`,
expErr: `d2/testdata/d2compiler/TestCompile/underscore_parent_root.d2:2:1: invalid underscore: no parent`,
},
{
name: "underscore_parent_middle_path",
@ -467,8 +480,7 @@ x: {
y._.z
}
`,
expErr: `d2/testdata/d2compiler/TestCompile/underscore_parent_middle_path.d2:3:3: parent "_" can only be used in the beginning of paths, e.g. "_.x"
`,
expErr: `d2/testdata/d2compiler/TestCompile/underscore_parent_middle_path.d2:3:5: parent "_" can only be used in the beginning of paths, e.g. "_.x"`,
},
{
name: "underscore_parent_sandwich_path",
@ -478,8 +490,7 @@ x: {
_.z._
}
`,
expErr: `d2/testdata/d2compiler/TestCompile/underscore_parent_sandwich_path.d2:3:3: parent "_" can only be used in the beginning of paths, e.g. "_.x"
`,
expErr: `d2/testdata/d2compiler/TestCompile/underscore_parent_sandwich_path.d2:3:7: parent "_" can only be used in the beginning of paths, e.g. "_.x"`,
},
{
name: "underscore_edge",
@ -996,8 +1007,7 @@ x -> y: {
text: `x: {shape: triangle}
`,
expErr: `d2/testdata/d2compiler/TestCompile/object_arrowhead_shape.d2:1:5: invalid shape, can only set "triangle" for arrowheads
`,
expErr: `d2/testdata/d2compiler/TestCompile/object_arrowhead_shape.d2:1:5: invalid shape, can only set "triangle" for arrowheads`,
},
{
name: "edge_flat_label_arrowhead",
@ -1083,8 +1093,7 @@ x -> y: {
space -> stars
}
`,
expErr: `d2/testdata/d2compiler/TestCompile/nested_edge.d2:1:1: edges cannot be nested within another edge
`,
expErr: `d2/testdata/d2compiler/TestCompile/nested_edge.d2:2:3: cannot create edge inside edge`,
},
{
name: "shape_edge_style",
@ -1094,8 +1103,7 @@ x: {
style.animated: true
}
`,
expErr: `d2/testdata/d2compiler/TestCompile/shape_edge_style.d2:3:2: key "animated" can only be applied to edges
`,
expErr: `d2/testdata/d2compiler/TestCompile/shape_edge_style.d2:3:2: key "animated" can only be applied to edges`,
},
{
name: "edge_chain_map",
@ -1351,8 +1359,7 @@ x -> y: {
z
}
`,
expErr: `d2/testdata/d2compiler/TestCompile/edge_map_non_reserved.d2:2:1: edge map keys must be reserved keywords
`,
expErr: `d2/testdata/d2compiler/TestCompile/edge_map_non_reserved.d2:3:3: edge map keys must be reserved keywords`,
},
{
name: "url_link",
@ -1397,8 +1404,7 @@ x -> y: {
text: `x.near: txop-center
`,
expErr: `d2/testdata/d2compiler/TestCompile/near_bad_constant.d2:1:1: near key "txop-center" must be the absolute path to a shape or one of the following constants: top-left, top-center, top-right, center-left, center-right, bottom-left, bottom-center, bottom-right
`,
expErr: `d2/testdata/d2compiler/TestCompile/near_bad_constant.d2:1:9: near key "txop-center" must be the absolute path to a shape or one of the following constants: top-left, top-center, top-right, center-left, center-right, bottom-left, bottom-center, bottom-right`,
},
{
name: "near_bad_container",
@ -1408,8 +1414,7 @@ x -> y: {
y
}
`,
expErr: `d2/testdata/d2compiler/TestCompile/near_bad_container.d2:1:1: constant near keys cannot be set on shapes with children
`,
expErr: `d2/testdata/d2compiler/TestCompile/near_bad_container.d2:2:9: constant near keys cannot be set on shapes with children`,
},
{
name: "near_bad_connected",
@ -1419,16 +1424,14 @@ x -> y: {
}
x -> y
`,
expErr: `d2/testdata/d2compiler/TestCompile/near_bad_connected.d2:1:1: constant near keys cannot be set on connected shapes
`,
expErr: `d2/testdata/d2compiler/TestCompile/near_bad_connected.d2:2:9: constant near keys cannot be set on connected shapes`,
},
{
name: "nested_near_constant",
text: `x.y.near: top-center
`,
expErr: `d2/testdata/d2compiler/TestCompile/nested_near_constant.d2:1:1: constant near keys can only be set on root level shapes
`,
expErr: `d2/testdata/d2compiler/TestCompile/nested_near_constant.d2:1:11: constant near keys can only be set on root level shapes`,
},
{
name: "reserved_icon_near_style",
@ -1474,17 +1477,14 @@ y
}
`,
expErr: `d2/testdata/d2compiler/TestCompile/errors/reserved_icon_style.d2:3:9: bad icon url "::????:::%%orange": parse "::????:::%%orange": missing protocol scheme
d2/testdata/d2compiler/TestCompile/errors/reserved_icon_style.d2:4:18: expected "opacity" to be a number between 0.0 and 1.0
d2/testdata/d2compiler/TestCompile/errors/reserved_icon_style.d2:5:18: expected "opacity" to be a number between 0.0 and 1.0
d2/testdata/d2compiler/TestCompile/errors/reserved_icon_style.d2:1:1: near key "y" must be the absolute path to a shape or one of the following constants: top-left, top-center, top-right, center-left, center-right, bottom-left, bottom-center, bottom-right
`,
d2/testdata/d2compiler/TestCompile/errors/reserved_icon_style.d2:2:9: near key "y" must be the absolute path to a shape or one of the following constants: top-left, top-center, top-right, center-left, center-right, bottom-left, bottom-center, bottom-right`,
},
{
name: "errors/missing_shape_icon",
text: `x.shape: image`,
expErr: `d2/testdata/d2compiler/TestCompile/errors/missing_shape_icon.d2:1:1: image shape must include an "icon" field
`,
text: `x.shape: image`,
expErr: `d2/testdata/d2compiler/TestCompile/errors/missing_shape_icon.d2:1:1: image shape must include an "icon" field`,
},
{
name: "edge_in_column",
@ -1493,6 +1493,36 @@ d2/testdata/d2compiler/TestCompile/errors/reserved_icon_style.d2:1:1: near key "
shape: sql_table
x: {p -> q}
}`,
expErr: `d2/testdata/d2compiler/TestCompile/edge_in_column.d2:3:7: sql_table columns cannot have children
d2/testdata/d2compiler/TestCompile/edge_in_column.d2:3:12: sql_table columns cannot have children`,
},
{
name: "no-nested-columns-sql",
text: `x: {
shape: sql_table
a -- b.b
}`,
expErr: `d2/testdata/d2compiler/TestCompile/no-nested-columns-sql.d2:3:10: sql_table columns cannot have children`,
},
{
name: "no-nested-columns-sql-2",
text: `x: {
shape: sql_table
a
}
x.a.b`,
expErr: `d2/testdata/d2compiler/TestCompile/no-nested-columns-sql-2.d2:5:5: sql_table columns cannot have children`,
},
{
name: "no-nested-columns-class",
text: `x: {
shape: class
a.a
}`,
expErr: `d2/testdata/d2compiler/TestCompile/no-nested-columns-class.d2:3:5: class fields cannot have children`,
},
{
name: "edge_to_style",
@ -1500,8 +1530,7 @@ d2/testdata/d2compiler/TestCompile/errors/reserved_icon_style.d2:1:1: near key "
text: `x: {style.opacity: 0.4}
y -> x.style
`,
expErr: `d2/testdata/d2compiler/TestCompile/edge_to_style.d2:2:1: cannot connect to reserved keyword
`,
expErr: `d2/testdata/d2compiler/TestCompile/edge_to_style.d2:2:8: reserved keywords are prohibited in edges`,
},
{
name: "escaped_id",
@ -1581,7 +1610,7 @@ b`, g.Objects[0].Attributes.Label.Value)
GetType(): string
style: {
opacity: 0.4
color: blue
font-color: blue
}
}
`,
@ -1680,10 +1709,9 @@ x.y -> a.b: {
{
name: "3d_oval",
text: `SVP1.style.shape: oval
text: `SVP1.shape: oval
SVP1.style.3d: true`,
expErr: `d2/testdata/d2compiler/TestCompile/3d_oval.d2:2:1: key "3d" can only be applied to squares and rectangles
`,
expErr: `d2/testdata/d2compiler/TestCompile/3d_oval.d2:2:1: key "3d" can only be applied to squares and rectangles`,
}, {
name: "edge_column_index",
text: `src: {
@ -1740,8 +1768,7 @@ dst.id <-> src.dst_id
}
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: `d2/testdata/d2compiler/TestCompile/leaky_sequence.d2:5:1: connections within sequence diagrams can connect only to other objects within the same sequence diagram`,
},
{
name: "sequence_scoping",
@ -1775,6 +1802,35 @@ choo: {
tassert.Equal(t, 3, len(g.Root.ChildrenArray))
},
},
{
name: "sequence_container",
text: `shape: sequence_diagram
x.y.q -> j.y.p
ok: {
x.y.q -> j.y.p
}
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
tassert.Equal(t, 7, len(g.Objects))
tassert.Equal(t, 3, len(g.Root.ChildrenArray))
},
},
{
name: "sequence_container_2",
text: `shape: sequence_diagram
x.y.q
ok: {
x.y.q -> j.y.p
meow
}
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
tassert.Equal(t, 8, len(g.Objects))
tassert.Equal(t, 2, len(g.Root.ChildrenArray))
},
},
{
name: "root_direction",
@ -1818,8 +1874,7 @@ choo: {
text: `x: {
direction: diagonal
}`,
expErr: `d2/testdata/d2compiler/TestCompile/invalid_direction.d2:2:14: direction must be one of up, down, right, left, got "diagonal"
`,
expErr: `d2/testdata/d2compiler/TestCompile/invalid_direction.d2:2:14: direction must be one of up, down, right, left, got "diagonal"`,
},
{
name: "self-referencing",
@ -1868,8 +1923,7 @@ choo: {
test_id: varchar(64) {constraint: [primary_key, foreign_key]}
}
`,
expErr: `d2/testdata/d2compiler/TestCompile/sql-panic.d2:3:27: constraint value must be a string
`,
expErr: `d2/testdata/d2compiler/TestCompile/sql-panic.d2:3:27: reserved field constraint does not accept composite`,
},
{
name: "wrong_column_index",
@ -1939,3 +1993,167 @@ Chinchillas_Collectibles.chinchilla -> Chinchillas.id`,
})
}
}
func TestCompile2(t *testing.T) {
t.Parallel()
t.Run("boards", testBoards)
t.Run("seqdiagrams", testSeqDiagrams)
}
func testBoards(t *testing.T) {
t.Parallel()
tca := []struct {
name string
run func(t *testing.T)
}{
{
name: "root",
run: func(t *testing.T) {
g := assertCompile(t, `base
layers: {
one: {
santa
}
two: {
clause
}
}
`, "")
assert.JSON(t, 2, len(g.Layers))
assert.JSON(t, "one", g.Layers[0].Name)
assert.JSON(t, "two", g.Layers[1].Name)
},
},
{
name: "recursive",
run: func(t *testing.T) {
g := assertCompile(t, `base
layers: {
one: {
santa
}
two: {
clause
steps: {
seinfeld: {
reindeer
}
missoula: {
montana
}
}
}
}
`, "")
assert.Equal(t, 2, len(g.Layers))
assert.Equal(t, "one", g.Layers[0].Name)
assert.Equal(t, "two", g.Layers[1].Name)
assert.Equal(t, 2, len(g.Layers[1].Steps))
},
},
{
name: "errs/duplicate_board",
run: func(t *testing.T) {
assertCompile(t, `base
layers: {
one: {
santa
}
}
steps: {
one: {
clause
}
}
`, `d2/testdata/d2compiler/TestCompile2/boards/errs/duplicate_board.d2:9:2: board name one already used by another board`)
},
},
}
for _, tc := range tca {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
tc.run(t)
})
}
}
func testSeqDiagrams(t *testing.T) {
t.Parallel()
t.Run("errs", func(t *testing.T) {
t.Parallel()
tca := []struct {
name string
skip bool
run func(t *testing.T)
}{
{
name: "sequence_diagram_edge_between_edge_groups",
// New sequence diagram scoping implementation is disabled.
skip: true,
run: func(t *testing.T) {
assertCompile(t, `
Office chatter: {
shape: sequence_diagram
alice: Alice
bob: Bobby
awkward small talk: {
alice -> bob: uhm, hi
bob -> alice: oh, hello
icebreaker attempt: {
alice -> bob: what did you have for lunch?
}
unfortunate outcome: {
bob -> alice: that's personal
}
}
awkward small talk.icebreaker attempt.alice -> awkward small talk.unfortunate outcome.bob
}
`, "d2/testdata/d2compiler/TestCompile2/seqdiagrams/errs/sequence_diagram_edge_between_edge_groups.d2:16:3: edges between edge groups are not allowed")
},
},
}
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 {
d2Path := fmt.Sprintf("d2/testdata/d2compiler/%v.d2", t.Name())
g, err := d2compiler.Compile(d2Path, strings.NewReader(text), nil)
if expErr != "" {
assert.Error(t, err)
assert.ErrorString(t, err, expErr)
} else {
assert.Success(t, err)
}
got := struct {
Graph *d2graph.Graph `json:"graph"`
Err error `json:"err"`
}{
Graph: g,
Err: err,
}
err = diff.TestdataJSON(filepath.Join("..", "testdata", "d2compiler", t.Name()), got)
assert.Success(t, err)
return g
}

View file

@ -4,15 +4,17 @@ import (
"context"
"strconv"
"oss.terrastruct.com/util-go/go2"
"oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/d2renderers/d2fonts"
"oss.terrastruct.com/d2/d2target"
"oss.terrastruct.com/d2/lib/color"
"oss.terrastruct.com/util-go/go2"
)
func Export(ctx context.Context, g *d2graph.Graph, fontFamily *d2fonts.FontFamily) (*d2target.Diagram, error) {
diagram := d2target.NewDiagram()
diagram.Name = g.Name
if fontFamily == nil {
fontFamily = go2.Pointer(d2fonts.SourceSansPro)
}
@ -131,16 +133,20 @@ func toShape(obj *d2graph.Object) d2target.Shape {
case d2target.ShapeClass:
shape.Class = *obj.Class
// The label is the header for classes and tables, which is set in client to be 4 px larger than the object's set font size
shape.FontSize -= 4
shape.FontSize -= d2target.HeaderFontAdd
case d2target.ShapeSQLTable:
shape.SQLTable = *obj.SQLTable
shape.FontSize -= 4
shape.FontSize -= d2target.HeaderFontAdd
}
shape.Label = text.Text
shape.LabelWidth = text.Dimensions.Width
shape.LabelHeight = text.Dimensions.Height
if obj.LabelPosition != nil {
shape.LabelPosition = *obj.LabelPosition
if obj.IsSequenceDiagramGroup() {
shape.LabelFill = shape.Fill
}
}
shape.Tooltip = obj.Attributes.Tooltip
@ -157,7 +163,6 @@ func toConnection(edge *d2graph.Edge) d2target.Connection {
connection := d2target.BaseConnection()
connection.ID = edge.AbsID()
connection.ZIndex = edge.ZIndex
// edge.Edge.ID = go2.StringToIntHash(connection.ID)
text := edge.Text()
if edge.SrcArrow {

View file

@ -397,3 +397,15 @@ func (p *printer) edgeIndex(ei *d2ast.EdgeIndex) {
}
p.sb.WriteByte(']')
}
func KeyPath(kp *d2ast.KeyPath) (ida []string) {
for _, s := range kp.Path {
// We format each string of the key to ensure the resulting strings can be parsed
// correctly.
n := &d2ast.KeyPath{
Path: []*d2ast.StringBox{d2ast.MakeValueBox(d2ast.RawString(s.Unbox().ScalarString(), true)).StringBox()},
}
ida = append(ida, Format(n))
}
return ida
}

View file

@ -5,6 +5,7 @@ import (
"fmt"
"math"
"net/url"
"sort"
"strconv"
"strings"
@ -18,31 +19,34 @@ import (
"oss.terrastruct.com/d2/d2target"
"oss.terrastruct.com/d2/lib/color"
"oss.terrastruct.com/d2/lib/geo"
"oss.terrastruct.com/d2/lib/shape"
"oss.terrastruct.com/d2/lib/textmeasure"
)
const INNER_LABEL_PADDING int = 5
const DEFAULT_SHAPE_PADDING = 100.
const DEFAULT_SHAPE_SIZE = 100.
const MIN_SHAPE_SIZE = 5
// TODO: Refactor with a light abstract layer on top of AST implementing scenarios,
// variables, imports, substitutions and then a final set of structures representing
// a final graph.
type Graph struct {
AST *d2ast.Map `json:"ast"`
Name string `json:"name"`
AST *d2ast.Map `json:"ast"`
Root *Object `json:"root"`
Edges []*Edge `json:"edges"`
Objects []*Object `json:"objects"`
Layers []*Graph `json:"layers,omitempty"`
Scenarios []*Graph `json:"scenarios,omitempty"`
Steps []*Graph `json:"steps,omitempty"`
}
func NewGraph(ast *d2ast.Map) *Graph {
d := &Graph{
AST: ast,
}
func NewGraph() *Graph {
d := &Graph{}
d.Root = &Object{
Graph: d,
Parent: nil,
Children: make(map[string]*Object),
Graph: d,
Parent: nil,
Children: make(map[string]*Object),
Attributes: &Attributes{},
}
return d
}
@ -82,7 +86,7 @@ type Object struct {
Children map[string]*Object `json:"-"`
ChildrenArray []*Object `json:"-"`
Attributes Attributes `json:"attributes"`
Attributes *Attributes `json:"attributes,omitempty"`
ZIndex int `json:"zIndex"`
}
@ -94,10 +98,12 @@ type Attributes struct {
Tooltip string `json:"tooltip,omitempty"`
Link string `json:"link,omitempty"`
// Only applicable for images right now
Width *Scalar `json:"width,omitempty"`
Height *Scalar `json:"height,omitempty"`
Top *Scalar `json:"top,omitempty"`
Left *Scalar `json:"left,omitempty"`
// TODO consider separate Attributes struct for shape-specific and edge-specific
// Shapes only
NearKey *d2ast.KeyPath `json:"near_key"`
@ -105,7 +111,8 @@ type Attributes struct {
// TODO: default to ShapeRectangle instead of empty string
Shape Scalar `json:"shape"`
Direction Scalar `json:"direction"`
Direction Scalar `json:"direction"`
Constraint Scalar `json:"constraint"`
}
// TODO references at the root scope should have their Scope set to root graph AST
@ -116,9 +123,7 @@ type Reference struct {
MapKey *d2ast.Key `json:"-"`
MapKeyEdgeIndex int `json:"map_key_edge_index"`
Scope *d2ast.Map `json:"-"`
// The ScopeObj and UnresolvedScopeObj are the same except when the key contains underscores
ScopeObj *Object `json:"-"`
UnresolvedScopeObj *Object `json:"-"`
ScopeObj *Object `json:"-"`
}
func (r Reference) MapKeyEdgeDest() bool {
@ -462,6 +467,11 @@ func (obj *Object) Text() *d2target.MText {
isItalic = true
}
fontSize := d2fonts.FONT_SIZE_M
if obj.Class != nil || obj.SQLTable != nil {
fontSize = d2fonts.FONT_SIZE_L
}
if obj.OuterSequenceDiagram() == nil {
if obj.IsContainer() {
fontSize = obj.Level().LabelSize()
@ -474,7 +484,7 @@ func (obj *Object) Text() *d2target.MText {
}
// Class and Table objects have Label set to header
if obj.Class != nil || obj.SQLTable != nil {
fontSize = d2fonts.FONT_SIZE_XL
fontSize += d2target.HeaderFontAdd
}
if obj.Class != nil {
isBold = false
@ -500,10 +510,13 @@ func (obj *Object) newObject(id string) *Object {
child := &Object{
ID: id,
IDVal: idval,
Attributes: Attributes{
Attributes: &Attributes{
Label: Scalar{
Value: idval,
},
Shape: Scalar{
Value: d2target.ShapeRectangle,
},
},
Graph: obj.Graph,
@ -523,6 +536,9 @@ func (obj *Object) newObject(id string) *Object {
}
func (obj *Object) HasChild(ids []string) (*Object, bool) {
if len(ids) == 0 {
return obj, true
}
if len(ids) == 1 && ids[0] != "style" {
_, ok := ReservedKeywords[ids[0]]
if ok {
@ -544,6 +560,38 @@ func (obj *Object) HasChild(ids []string) (*Object, bool) {
return child, true
}
// Keep in sync with HasChild.
func (obj *Object) HasChildIDVal(ids []string) (*Object, bool) {
if len(ids) == 0 {
return obj, true
}
if len(ids) == 1 && ids[0] != "style" {
_, ok := ReservedKeywords[ids[0]]
if ok {
return obj, true
}
}
id := ids[0]
ids = ids[1:]
var child *Object
for _, ch2 := range obj.ChildrenArray {
if ch2.IDVal == id {
child = ch2
break
}
}
if child == nil {
return nil, false
}
if len(ids) >= 1 {
return child.HasChildIDVal(ids)
}
return child, true
}
func (obj *Object) HasEdge(mk *d2ast.Key) (*Edge, bool) {
ea, ok := obj.FindEdges(mk)
if !ok {
@ -557,6 +605,7 @@ func (obj *Object) HasEdge(mk *d2ast.Key) (*Edge, bool) {
return nil, false
}
// TODO: remove once not used anywhere
func ResolveUnderscoreKey(ida []string, obj *Object) (resolvedObj *Object, resolvedIDA []string, _ error) {
if len(ida) > 0 && !obj.IsSequenceDiagram() {
objSD := obj.OuterSequenceDiagram()
@ -637,34 +686,71 @@ func (obj *Object) FindEdges(mk *d2ast.Key) ([]*Edge, bool) {
return ea, true
}
func (obj *Object) ensureChildEdge(ida []string) *Object {
for i := range ida {
switch obj.Attributes.Shape.Value {
case d2target.ShapeClass, d2target.ShapeSQLTable:
// This will only be called for connecting edges where we want to truncate to the
// container.
return obj
default:
obj = obj.EnsureChild(ida[i : i+1])
}
}
return obj
}
// EnsureChild grabs the child by ids or creates it if it does not exist including all
// intermediate nodes.
func (obj *Object) EnsureChild(ids []string) *Object {
_, is := ReservedKeywordHolders[ids[0]]
if len(ids) == 1 && !is {
_, ok := ReservedKeywords[ids[0]]
func (obj *Object) EnsureChild(ida []string) *Object {
seq := obj.OuterSequenceDiagram()
if seq != nil {
for _, c := range seq.ChildrenArray {
if c.ID == ida[0] {
if obj.ID == ida[0] {
// In cases of a.a where EnsureChild is called on the parent a, the second a should
// be created as a child of a and not as a child of the diagram. This is super
// unfortunate code but alas.
break
}
obj = seq
break
}
}
}
if len(ida) == 0 {
return obj
}
_, is := ReservedKeywordHolders[ida[0]]
if len(ida) == 1 && !is {
_, ok := ReservedKeywords[ida[0]]
if ok {
return obj
}
}
id := ids[0]
ids = ids[1:]
id := ida[0]
ida = ida[1:]
if id == "_" {
return obj.Parent.EnsureChild(ida)
}
child, ok := obj.Children[strings.ToLower(id)]
if !ok {
child = obj.newObject(id)
}
if len(ids) >= 1 {
return child.EnsureChild(ids)
if len(ida) >= 1 {
return child.EnsureChild(ida)
}
return child
}
func (obj *Object) AppendReferences(ida []string, ref Reference, unresolvedObj *Object) {
ref.ScopeObj = obj
ref.UnresolvedScopeObj = unresolvedObj
ref.ScopeObj = unresolvedObj
numUnderscores := 0
for i := range ida {
if ida[i] == "_" {
@ -730,9 +816,14 @@ func (obj *Object) GetLabelSize(mtexts []*d2target.MText, ruler *textmeasure.Rul
return dims, nil
}
func (obj *Object) GetDefaultSize(mtexts []*d2target.MText, ruler *textmeasure.Ruler, fontFamily *d2fonts.FontFamily, labelDims d2target.TextDimensions) (*d2target.TextDimensions, error) {
func (obj *Object) GetDefaultSize(mtexts []*d2target.MText, ruler *textmeasure.Ruler, fontFamily *d2fonts.FontFamily, labelDims d2target.TextDimensions, withLabelPadding bool) (*d2target.TextDimensions, error) {
dims := d2target.TextDimensions{}
if withLabelPadding {
labelDims.Width += INNER_LABEL_PADDING
labelDims.Height += INNER_LABEL_PADDING
}
switch strings.ToLower(obj.Attributes.Shape.Value) {
default:
return d2target.NewTextDimensions(labelDims.Width, labelDims.Height), nil
@ -743,41 +834,45 @@ func (obj *Object) GetDefaultSize(mtexts []*d2target.MText, ruler *textmeasure.R
case d2target.ShapeClass:
maxWidth := go2.Max(12, labelDims.Width)
fontSize := d2fonts.FONT_SIZE_L
if obj.Attributes.Style.FontSize != nil {
fontSize, _ = strconv.Atoi(obj.Attributes.Style.FontSize.Value)
}
for _, f := range obj.Class.Fields {
fdims := GetTextDimensions(mtexts, ruler, f.Text(), go2.Pointer(d2fonts.SourceCodePro))
fdims := GetTextDimensions(mtexts, ruler, f.Text(fontSize), go2.Pointer(d2fonts.SourceCodePro))
if fdims == nil {
return nil, fmt.Errorf("dimensions for class field %#v not found", f.Text())
}
lineWidth := fdims.Width
if maxWidth < lineWidth {
maxWidth = lineWidth
return nil, fmt.Errorf("dimensions for class field %#v not found", f.Text(fontSize))
}
maxWidth = go2.Max(maxWidth, fdims.Width)
}
for _, m := range obj.Class.Methods {
mdims := GetTextDimensions(mtexts, ruler, m.Text(), go2.Pointer(d2fonts.SourceCodePro))
mdims := GetTextDimensions(mtexts, ruler, m.Text(fontSize), go2.Pointer(d2fonts.SourceCodePro))
if mdims == nil {
return nil, fmt.Errorf("dimensions for class method %#v not found", m.Text())
}
lineWidth := mdims.Width
if maxWidth < lineWidth {
maxWidth = lineWidth
return nil, fmt.Errorf("dimensions for class method %#v not found", m.Text(fontSize))
}
maxWidth = go2.Max(maxWidth, mdims.Width)
}
dims.Width = maxWidth
// ┌─PrefixWidth ┌─CenterPadding
// ┌─┬─┬───────┬──────┬───┬──┐
// │ + getJobs() Job[] │
// └─┴─┴───────┴──────┴───┴──┘
// └─PrefixPadding └──TypePadding
// ├───────┤ + ├───┤ = maxWidth
dims.Width = d2target.PrefixPadding + d2target.PrefixWidth + maxWidth + d2target.CenterPadding + d2target.TypePadding
// All rows should be the same height
var anyRowText *d2target.MText
if len(obj.Class.Fields) > 0 {
anyRowText = obj.Class.Fields[0].Text()
anyRowText = obj.Class.Fields[0].Text(fontSize)
} else if len(obj.Class.Methods) > 0 {
anyRowText = obj.Class.Methods[0].Text()
anyRowText = obj.Class.Methods[0].Text(fontSize)
}
if anyRowText != nil {
// 10px of padding top and bottom so text doesn't look squished
rowHeight := GetTextDimensions(mtexts, ruler, anyRowText, go2.Pointer(d2fonts.SourceCodePro)).Height + 20
rowHeight := GetTextDimensions(mtexts, ruler, anyRowText, go2.Pointer(d2fonts.SourceCodePro)).Height + d2target.VerticalPadding
dims.Height = rowHeight * (len(obj.Class.Fields) + len(obj.Class.Methods) + 2)
} else {
dims.Height = go2.Max(12, labelDims.Height)
dims.Height = 2*go2.Max(12, labelDims.Height) + d2target.VerticalPadding
}
case d2target.ShapeSQLTable:
@ -785,10 +880,16 @@ func (obj *Object) GetDefaultSize(mtexts []*d2target.MText, ruler *textmeasure.R
maxTypeWidth := 0
constraintWidth := 0
colFontSize := d2fonts.FONT_SIZE_L
if obj.Attributes.Style.FontSize != nil {
colFontSize, _ = strconv.Atoi(obj.Attributes.Style.FontSize.Value)
}
for i := range obj.SQLTable.Columns {
// Note: we want to set dimensions of actual column not the for loop copy of the struct
c := &obj.SQLTable.Columns[i]
ctexts := c.Texts()
ctexts := c.Texts(colFontSize)
nameDims := GetTextDimensions(mtexts, ruler, ctexts[0], fontFamily)
if nameDims == nil {
@ -796,9 +897,7 @@ func (obj *Object) GetDefaultSize(mtexts []*d2target.MText, ruler *textmeasure.R
}
c.Name.LabelWidth = nameDims.Width
c.Name.LabelHeight = nameDims.Height
if maxNameWidth < nameDims.Width {
maxNameWidth = nameDims.Width
}
maxNameWidth = go2.Max(maxNameWidth, nameDims.Width)
typeDims := GetTextDimensions(mtexts, ruler, ctexts[1], fontFamily)
if typeDims == nil {
@ -809,6 +908,7 @@ func (obj *Object) GetDefaultSize(mtexts []*d2target.MText, ruler *textmeasure.R
if maxTypeWidth < typeDims.Width {
maxTypeWidth = typeDims.Width
}
maxTypeWidth = go2.Max(maxTypeWidth, typeDims.Width)
if c.Constraint != "" {
// covers UNQ constraint with padding
@ -826,21 +926,6 @@ func (obj *Object) GetDefaultSize(mtexts []*d2target.MText, ruler *textmeasure.R
return &dims, nil
}
func (obj *Object) GetPadding() (x, y float64) {
switch strings.ToLower(obj.Attributes.Shape.Value) {
case d2target.ShapeImage,
d2target.ShapeSQLTable,
d2target.ShapeText,
d2target.ShapeCode:
return 0., 0.
case d2target.ShapeClass:
// TODO fix class row width measurements (see SQL table)
return 100., 0.
default:
return DEFAULT_SHAPE_PADDING, DEFAULT_SHAPE_PADDING
}
}
type Edge struct {
Index int `json:"index"`
@ -866,7 +951,7 @@ type Edge struct {
DstArrowhead *Attributes `json:"dstArrowhead,omitempty"`
References []EdgeReference `json:"references,omitempty"`
Attributes Attributes `json:"attributes"`
Attributes *Attributes `json:"attributes,omitempty"`
ZIndex int `json:"zIndex"`
}
@ -938,15 +1023,6 @@ func (e *Edge) AbsID() string {
}
func (obj *Object) Connect(srcID, dstID []string, srcArrow, dstArrow bool, label string) (*Edge, error) {
srcObj, srcID, err := ResolveUnderscoreKey(srcID, obj)
if err != nil {
return nil, err
}
dstObj, dstID, err := ResolveUnderscoreKey(dstID, obj)
if err != nil {
return nil, err
}
for _, id := range [][]string{srcID, dstID} {
for _, p := range id {
if _, ok := ReservedKeywords[p]; ok {
@ -955,15 +1031,15 @@ func (obj *Object) Connect(srcID, dstID []string, srcArrow, dstArrow bool, label
}
}
src := srcObj.EnsureChild(srcID)
dst := dstObj.EnsureChild(dstID)
src := obj.ensureChildEdge(srcID)
dst := obj.ensureChildEdge(dstID)
if src.OuterSequenceDiagram() != dst.OuterSequenceDiagram() {
return nil, errors.New("connections within sequence diagrams can connect only to other objects within the same sequence diagram")
}
edge := &Edge{
Attributes: Attributes{
e := &Edge{
Attributes: &Attributes{
Label: Scalar{
Value: label,
},
@ -973,10 +1049,47 @@ func (obj *Object) Connect(srcID, dstID []string, srcArrow, dstArrow bool, label
Dst: dst,
DstArrow: dstArrow,
}
edge.initIndex()
e.initIndex()
obj.Graph.Edges = append(obj.Graph.Edges, edge)
return edge, nil
addSQLTableColumnIndices(e, srcID, dstID, obj, src, dst)
obj.Graph.Edges = append(obj.Graph.Edges, e)
return e, nil
}
func addSQLTableColumnIndices(e *Edge, srcID, dstID []string, obj, src, dst *Object) {
if src.Attributes.Shape.Value == d2target.ShapeSQLTable {
if src == dst {
// Ignore edge to column inside table.
return
}
objAbsID := obj.AbsIDArray()
srcAbsID := src.AbsIDArray()
if len(objAbsID)+len(srcID) > len(srcAbsID) {
for i, d2col := range src.SQLTable.Columns {
if d2col.Name.Label == srcID[len(srcID)-1] {
d2col.Reference = dst.AbsID()
e.SrcTableColumnIndex = new(int)
*e.SrcTableColumnIndex = i
break
}
}
}
}
if dst.Attributes.Shape.Value == d2target.ShapeSQLTable {
objAbsID := obj.AbsIDArray()
dstAbsID := dst.AbsIDArray()
if len(objAbsID)+len(dstID) > len(dstAbsID) {
for i, d2col := range dst.SQLTable.Columns {
if d2col.Name.Label == dstID[len(dstID)-1] {
d2col.Reference = dst.AbsID()
e.DstTableColumnIndex = new(int)
*e.DstTableColumnIndex = i
break
}
}
}
}
}
// TODO: Treat undirectional/bidirectional edge here and in HasEdge flipped. Same with
@ -1108,29 +1221,41 @@ func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler
desiredHeight, _ = strconv.Atoi(obj.Attributes.Height.Value)
}
dslShape := strings.ToLower(obj.Attributes.Shape.Value)
if obj.Attributes.Label.Value == "" &&
obj.Attributes.Shape.Value != d2target.ShapeImage &&
obj.Attributes.Shape.Value != d2target.ShapeSQLTable &&
obj.Attributes.Shape.Value != d2target.ShapeClass {
obj.Width = DEFAULT_SHAPE_PADDING
obj.Height = DEFAULT_SHAPE_PADDING
if desiredWidth != 0 {
obj.Width = float64(desiredWidth)
}
if desiredHeight != 0 {
obj.Height = float64(desiredHeight)
dslShape != d2target.ShapeImage &&
dslShape != d2target.ShapeSQLTable &&
dslShape != d2target.ShapeClass {
if dslShape == d2target.ShapeCircle || dslShape == d2target.ShapeSquare {
sideLength := DEFAULT_SHAPE_SIZE
if desiredWidth != 0 || desiredHeight != 0 {
sideLength = float64(go2.Max(desiredWidth, desiredHeight))
}
obj.Width = sideLength
obj.Height = sideLength
} else {
obj.Width = DEFAULT_SHAPE_SIZE
obj.Height = DEFAULT_SHAPE_SIZE
if desiredWidth != 0 {
obj.Width = float64(desiredWidth)
}
if desiredHeight != 0 {
obj.Height = float64(desiredHeight)
}
}
continue
}
shapeType := strings.ToLower(obj.Attributes.Shape.Value)
labelDims, err := obj.GetLabelSize(mtexts, ruler, fontFamily)
if err != nil {
return err
}
obj.LabelDimensions = *labelDims
switch shapeType {
switch dslShape {
case d2target.ShapeText, d2target.ShapeClass, d2target.ShapeSQLTable, d2target.ShapeCode:
// no labels
default:
@ -1140,39 +1265,76 @@ func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler
}
}
if shapeType != d2target.ShapeText && obj.Attributes.Label.Value != "" {
labelDims.Width += INNER_LABEL_PADDING
labelDims.Height += INNER_LABEL_PADDING
}
obj.LabelDimensions = *labelDims
defaultDims, err := obj.GetDefaultSize(mtexts, ruler, fontFamily, *labelDims)
// if there is a desired width or height, fit to content box without inner label padding for smallest minimum size
withInnerLabelPadding := desiredWidth == 0 && desiredHeight == 0 &&
dslShape != d2target.ShapeText && obj.Attributes.Label.Value != ""
defaultDims, err := obj.GetDefaultSize(mtexts, ruler, fontFamily, *labelDims, withInnerLabelPadding)
if err != nil {
return err
}
obj.Width = float64(go2.Max(defaultDims.Width, desiredWidth))
obj.Height = float64(go2.Max(defaultDims.Height, desiredHeight))
paddingX, paddingY := obj.GetPadding()
switch shapeType {
case d2target.ShapeSquare, d2target.ShapeCircle:
if desiredWidth != 0 || desiredHeight != 0 {
paddingX = 0.
paddingY = 0.
}
sideLength := math.Max(obj.Width+paddingX, obj.Height+paddingY)
obj.Width = sideLength
obj.Height = sideLength
default:
if dslShape == d2target.ShapeImage {
if desiredWidth == 0 {
obj.Width += float64(paddingX)
desiredWidth = defaultDims.Width
}
if desiredHeight == 0 {
obj.Height += float64(paddingY)
desiredHeight = defaultDims.Height
}
obj.Width = float64(go2.Max(MIN_SHAPE_SIZE, desiredWidth))
obj.Height = float64(go2.Max(MIN_SHAPE_SIZE, desiredHeight))
// images don't need further processing
continue
}
contentBox := geo.NewBox(geo.NewPoint(0, 0), float64(defaultDims.Width), float64(defaultDims.Height))
shapeType := d2target.DSL_SHAPE_TO_SHAPE_TYPE[dslShape]
s := shape.NewShape(shapeType, contentBox)
paddingX, paddingY := s.GetDefaultPadding()
if desiredWidth != 0 {
paddingX = 0.
}
if desiredHeight != 0 {
paddingY = 0.
}
// give shapes with icons extra padding to fit their label
if obj.Attributes.Icon != nil {
labelHeight := float64(labelDims.Height + INNER_LABEL_PADDING)
// Evenly pad enough to fit label above icon
if desiredWidth == 0 {
paddingX += labelHeight
}
if desiredHeight == 0 {
paddingY += labelHeight
}
}
if desiredWidth == 0 {
switch shapeType {
case shape.TABLE_TYPE, shape.CLASS_TYPE, shape.CODE_TYPE, shape.IMAGE_TYPE:
default:
if obj.Attributes.Link != "" {
paddingX += 32
}
if obj.Attributes.Tooltip != "" {
paddingX += 32
}
}
}
fitWidth, fitHeight := s.GetDimensionsToFit(contentBox.Width, contentBox.Height, paddingX, paddingY)
obj.Width = math.Max(float64(desiredWidth), fitWidth)
obj.Height = math.Max(float64(desiredHeight), fitHeight)
if s.AspectRatio1() {
sideLength := math.Max(obj.Width, obj.Height)
obj.Width = sideLength
obj.Height = sideLength
} else if desiredHeight == 0 || desiredWidth == 0 {
switch s.GetType() {
case shape.PERSON_TYPE:
obj.Width, obj.Height = shape.LimitAR(obj.Width, obj.Height, shape.PERSON_AR_LIMIT)
case shape.OVAL_TYPE:
obj.Width, obj.Height = shape.LimitAR(obj.Width, obj.Height, shape.OVAL_AR_LIMIT)
}
}
}
@ -1218,15 +1380,23 @@ func (g *Graph) Texts() []*d2target.MText {
texts = appendTextDedup(texts, obj.Text())
}
if obj.Class != nil {
fontSize := d2fonts.FONT_SIZE_L
if obj.Attributes.Style.FontSize != nil {
fontSize, _ = strconv.Atoi(obj.Attributes.Style.FontSize.Value)
}
for _, field := range obj.Class.Fields {
texts = appendTextDedup(texts, field.Text())
texts = appendTextDedup(texts, field.Text(fontSize))
}
for _, method := range obj.Class.Methods {
texts = appendTextDedup(texts, method.Text())
texts = appendTextDedup(texts, method.Text(fontSize))
}
} else if obj.SQLTable != nil {
colFontSize := d2fonts.FONT_SIZE_L
if obj.Attributes.Style.FontSize != nil {
colFontSize, _ = strconv.Atoi(obj.Attributes.Style.FontSize.Value)
}
for _, column := range obj.SQLTable.Columns {
for _, t := range column.Texts() {
for _, t := range column.Texts(colFontSize) {
texts = appendTextDedup(texts, t)
}
}
@ -1252,19 +1422,17 @@ func (g *Graph) Texts() []*d2target.MText {
}
func Key(k *d2ast.KeyPath) []string {
var ids []string
for _, s := range k.Path {
// We format each string of the key to ensure the resulting strings can be parsed
// correctly.
n := &d2ast.KeyPath{
Path: []*d2ast.StringBox{d2ast.MakeValueBox(d2ast.RawString(s.Unbox().ScalarString(), true)).StringBox()},
}
ids = append(ids, d2format.Format(n))
}
return ids
return d2format.KeyPath(k)
}
var ReservedKeywords = map[string]struct{}{
// All reserved keywords. See init below.
var ReservedKeywords map[string]struct{}
// All reserved keywords not including style keywords.
var ReservedKeywords2 map[string]struct{}
// Non Style/Holder keywords.
var SimpleReservedKeywords = map[string]struct{}{
"label": {},
"desc": {},
"shape": {},
@ -1276,6 +1444,8 @@ var ReservedKeywords = map[string]struct{}{
"width": {},
"height": {},
"direction": {},
"top": {},
"left": {},
}
// ReservedKeywordHolders are reserved keywords that are meaningless on its own and exist solely to hold a set of reserved keywords
@ -1331,15 +1501,88 @@ var NearConstantsArray = []string{
}
var NearConstants map[string]struct{}
// BoardKeywords contains the keywords that create new boards.
var BoardKeywords = map[string]struct{}{
"layers": {},
"scenarios": {},
"steps": {},
}
func init() {
ReservedKeywords = make(map[string]struct{})
for k, v := range SimpleReservedKeywords {
ReservedKeywords[k] = v
}
for k, v := range StyleKeywords {
ReservedKeywords[k] = v
}
for k, v := range ReservedKeywordHolders {
ReservedKeywords[k] = v
}
for k, v := range BoardKeywords {
ReservedKeywords[k] = v
}
ReservedKeywords2 = make(map[string]struct{})
for k, v := range SimpleReservedKeywords {
ReservedKeywords2[k] = v
}
for k, v := range ReservedKeywordHolders {
ReservedKeywords2[k] = v
}
for k, v := range BoardKeywords {
ReservedKeywords2[k] = v
}
NearConstants = make(map[string]struct{}, len(NearConstantsArray))
for _, k := range NearConstantsArray {
NearConstants[k] = struct{}{}
}
}
func (g *Graph) GetBoard(name string) *Graph {
for _, l := range g.Layers {
if l.Name == name {
return l
}
}
for _, l := range g.Scenarios {
if l.Name == name {
return l
}
}
for _, l := range g.Steps {
if l.Name == name {
return l
}
}
return nil
}
func (g *Graph) SortObjectsByAST() {
objects := append([]*Object(nil), g.Objects...)
sort.Slice(objects, func(i, j int) bool {
o1 := objects[i]
o2 := objects[j]
if len(o1.References) == 0 || len(o2.References) == 0 {
return i < j
}
r1 := o1.References[0]
r2 := o2.References[0]
return r1.Key.Path[r1.KeyPathIndex].Unbox().GetRange().Before(r2.Key.Path[r2.KeyPathIndex].Unbox().GetRange())
})
g.Objects = objects
}
func (g *Graph) SortEdgesByAST() {
edges := append([]*Edge(nil), g.Edges...)
sort.Slice(edges, func(i, j int) bool {
e1 := edges[i]
e2 := edges[j]
if len(e1.References) == 0 || len(e2.References) == 0 {
return i < j
}
return e1.References[0].Edge.Range.Before(e2.References[0].Edge.Range)
})
g.Edges = edges
}

View file

@ -3,7 +3,7 @@ package d2graph
import "oss.terrastruct.com/d2/d2target"
func (obj *Object) IsSequenceDiagram() bool {
return obj != nil && obj.Attributes.Shape.Value == d2target.ShapeSequenceDiagram
return obj != nil && obj.Attributes != nil && obj.Attributes.Shape.Value == d2target.ShapeSequenceDiagram
}
func (obj *Object) OuterSequenceDiagram() *Object {
@ -65,7 +65,7 @@ func (obj *Object) ContainsAnyObject(objects []*Object) bool {
func (o *Object) ContainedBy(obj *Object) bool {
for _, ref := range o.References {
curr := ref.UnresolvedScopeObj
curr := ref.ScopeObj
for curr != nil {
if curr == obj {
return true

View file

@ -2,8 +2,10 @@ package d2graph
import (
"encoding/json"
"fmt"
"strings"
"oss.terrastruct.com/d2/d2target"
"oss.terrastruct.com/util-go/go2"
)
@ -24,10 +26,10 @@ func DeserializeGraph(bytes []byte, g *Graph) error {
return err
}
g.Root = &Object{
Graph: g,
Children: make(map[string]*Object),
}
var root Object
convert(sg.Root, &root)
g.Root = &root
root.Graph = g
idToObj := make(map[string]*Object)
idToObj[""] = g.Root
@ -49,7 +51,7 @@ func DeserializeGraph(bytes []byte, g *Graph) error {
for _, id := range so["ChildrenArray"].([]interface{}) {
o := idToObj[id.(string)]
childrenArray = append(childrenArray, o)
children[strings.ToLower(id.(string))] = o
children[strings.ToLower(o.ID)] = o
o.Parent = idToObj[so["AbsID"].(string)]
}
@ -158,3 +160,316 @@ func convert[T, Q any](from T, to *Q) error {
}
return nil
}
func CompareSerializedGraph(g, other *Graph) error {
if len(g.Objects) != len(other.Objects) {
return fmt.Errorf("object count differs: g=%d, other=%d", len(g.Objects), len(other.Objects))
}
if len(g.Edges) != len(other.Edges) {
return fmt.Errorf("edge count differs: g=%d, other=%d", len(g.Edges), len(other.Edges))
}
if err := CompareSerializedObject(g.Root, other.Root); err != nil {
return fmt.Errorf("root differs: %v", err)
}
for i := 0; i < len(g.Objects); i++ {
if err := CompareSerializedObject(g.Objects[i], other.Objects[i]); err != nil {
return fmt.Errorf(
"objects differ at %d [g=%s, other=%s]: %v",
i,
g.Objects[i].ID,
other.Objects[i].ID,
err,
)
}
}
for i := 0; i < len(g.Edges); i++ {
if err := CompareSerializedEdge(g.Edges[i], other.Edges[i]); err != nil {
return fmt.Errorf(
"edges differ at %d [g=%s, other=%s]: %v",
i,
g.Edges[i].AbsID(),
other.Edges[i].AbsID(),
err,
)
}
}
return nil
}
func CompareSerializedObject(obj, other *Object) error {
if obj != nil && other == nil {
return fmt.Errorf("other is nil")
} else if obj == nil && other != nil {
return fmt.Errorf("obj is nil")
} else if obj == nil {
// both are nil
return nil
}
if obj.ID != other.ID {
return fmt.Errorf("ids differ: obj=%s, other=%s", obj.ID, other.ID)
}
if obj.AbsID() != other.AbsID() {
return fmt.Errorf("absolute ids differ: obj=%s, other=%s", obj.AbsID(), other.AbsID())
}
if obj.Box != nil && other.Box == nil {
return fmt.Errorf("other should have a box")
} else if obj.Box == nil && other.Box != nil {
return fmt.Errorf("other should not have a box")
} else if obj.Box != nil {
if obj.Width != other.Width {
return fmt.Errorf("widths differ: obj=%f, other=%f", obj.Width, other.Width)
}
if obj.Height != other.Height {
return fmt.Errorf("heights differ: obj=%f, other=%f", obj.Height, other.Height)
}
}
if obj.Parent != nil && other.Parent == nil {
return fmt.Errorf("other should have a parent")
} else if obj.Parent == nil && other.Parent != nil {
return fmt.Errorf("other should not have a parent")
} else if obj.Parent != nil && obj.Parent.ID != other.Parent.ID {
return fmt.Errorf("parent differs: obj=%s, other=%s", obj.Parent.ID, other.Parent.ID)
}
if len(obj.Children) != len(other.Children) {
return fmt.Errorf("children count differs: obj=%d, other=%d", len(obj.Children), len(other.Children))
}
for childID, objChild := range obj.Children {
if otherChild, exists := other.Children[childID]; exists {
if err := CompareSerializedObject(objChild, otherChild); err != nil {
return fmt.Errorf("children differ at key %s: %v", childID, err)
}
} else {
return fmt.Errorf("child %s does not exist in other", childID)
}
}
if len(obj.ChildrenArray) != len(other.ChildrenArray) {
return fmt.Errorf("childrenArray count differs: obj=%d, other=%d", len(obj.ChildrenArray), len(other.ChildrenArray))
}
for i := 0; i < len(obj.ChildrenArray); i++ {
if err := CompareSerializedObject(obj.ChildrenArray[i], other.ChildrenArray[i]); err != nil {
return fmt.Errorf("childrenArray differs at %d: %v", i, err)
}
}
if obj.Attributes != nil && other.Attributes == nil {
return fmt.Errorf("other should have attributes")
} else if obj.Attributes == nil && other.Attributes != nil {
return fmt.Errorf("other should not have attributes")
} else if obj.Attributes != nil {
if d2target.IsShape(obj.Attributes.Shape.Value) != d2target.IsShape(other.Attributes.Shape.Value) {
return fmt.Errorf(
"shapes differ: obj=%s, other=%s",
obj.Attributes.Shape.Value,
other.Attributes.Shape.Value,
)
}
if obj.Attributes.Icon == nil && other.Attributes.Icon != nil {
return fmt.Errorf("other does not have an icon")
} else if obj.Attributes.Icon != nil && other.Attributes.Icon == nil {
return fmt.Errorf("obj does not have an icon")
}
if obj.Attributes.Direction.Value != other.Attributes.Direction.Value {
return fmt.Errorf(
"directions differ: obj=%s, other=%s",
obj.Attributes.Direction.Value,
other.Attributes.Direction.Value,
)
}
if obj.Attributes.Label.Value != other.Attributes.Label.Value {
return fmt.Errorf(
"labels differ: obj=%s, other=%s",
obj.Attributes.Label.Value,
other.Attributes.Label.Value,
)
}
if obj.Attributes.NearKey != nil {
if other.Attributes.NearKey == nil {
return fmt.Errorf("other does not have near")
}
objKey := strings.Join(Key(obj.Attributes.NearKey), ".")
deserKey := strings.Join(Key(other.Attributes.NearKey), ".")
if objKey != deserKey {
return fmt.Errorf(
"near differs: obj=%s, other=%s",
objKey,
deserKey,
)
}
} else if other.Attributes.NearKey != nil {
return fmt.Errorf("other should not have near")
}
}
if obj.SQLTable == nil && other.SQLTable != nil {
return fmt.Errorf("other is not a sql table")
} else if obj.SQLTable != nil && other.SQLTable == nil {
return fmt.Errorf("obj is not a sql table")
}
if obj.SQLTable != nil {
if len(obj.SQLTable.Columns) != len(other.SQLTable.Columns) {
return fmt.Errorf(
"table columns count differ: obj=%d, other=%d",
len(obj.SQLTable.Columns),
len(other.SQLTable.Columns),
)
}
}
if obj.LabelWidth != nil {
if other.LabelWidth == nil {
return fmt.Errorf("other does not have a label width")
}
if *obj.LabelWidth != *other.LabelWidth {
return fmt.Errorf(
"label widths differ: obj=%d, other=%d",
*obj.LabelWidth,
*other.LabelWidth,
)
}
} else if other.LabelWidth != nil {
return fmt.Errorf("other should not have label width")
}
if obj.LabelHeight != nil {
if other.LabelHeight == nil {
return fmt.Errorf("other does not have a label height")
}
if *obj.LabelHeight != *other.LabelHeight {
return fmt.Errorf(
"label heights differ: obj=%d, other=%d",
*obj.LabelHeight,
*other.LabelHeight,
)
}
} else if other.LabelHeight != nil {
return fmt.Errorf("other should not have label height")
}
return nil
}
func CompareSerializedEdge(edge, other *Edge) error {
if edge.AbsID() != other.AbsID() {
return fmt.Errorf(
"absolute ids differ: edge=%s, other=%s",
edge.AbsID(),
other.AbsID(),
)
}
if edge.Src.AbsID() != other.Src.AbsID() {
return fmt.Errorf(
"sources differ: edge=%s, other=%s",
edge.Src.AbsID(),
other.Src.AbsID(),
)
}
if edge.Dst.AbsID() != other.Dst.AbsID() {
return fmt.Errorf(
"targets differ: edge=%s, other=%s",
edge.Dst.AbsID(),
other.Dst.AbsID(),
)
}
if edge.SrcArrow != other.SrcArrow {
return fmt.Errorf(
"source arrows differ: edge=%t, other=%t",
edge.SrcArrow,
other.SrcArrow,
)
}
if edge.DstArrow != other.DstArrow {
return fmt.Errorf(
"target arrows differ: edge=%t, other=%t",
edge.DstArrow,
other.DstArrow,
)
}
if edge.MinWidth != other.MinWidth {
return fmt.Errorf(
"min width differs: edge=%d, other=%d",
edge.MinWidth,
other.MinWidth,
)
}
if edge.MinHeight != other.MinHeight {
return fmt.Errorf(
"min height differs: edge=%d, other=%d",
edge.MinHeight,
other.MinHeight,
)
}
if edge.Attributes.Label.Value != other.Attributes.Label.Value {
return fmt.Errorf(
"labels differ: edge=%s, other=%s",
edge.Attributes.Label.Value,
other.Attributes.Label.Value,
)
}
if edge.LabelDimensions.Width != other.LabelDimensions.Width {
return fmt.Errorf(
"label width differs: edge=%d, other=%d",
edge.LabelDimensions.Width,
other.LabelDimensions.Width,
)
}
if edge.LabelDimensions.Height != other.LabelDimensions.Height {
return fmt.Errorf(
"label hieght differs: edge=%d, other=%d",
edge.LabelDimensions.Height,
other.LabelDimensions.Height,
)
}
if edge.SrcTableColumnIndex != nil && other.SrcTableColumnIndex == nil {
return fmt.Errorf("other should have src column index")
} else if other.SrcTableColumnIndex != nil && edge.SrcTableColumnIndex == nil {
return fmt.Errorf("other should not have src column index")
} else if other.SrcTableColumnIndex != nil {
edgeColumn := *edge.SrcTableColumnIndex
otherColumn := *other.SrcTableColumnIndex
if edgeColumn != otherColumn {
return fmt.Errorf("src column differs: edge=%d, other=%d", edgeColumn, otherColumn)
}
}
if edge.DstTableColumnIndex != nil && other.DstTableColumnIndex == nil {
return fmt.Errorf("other should have dst column index")
} else if other.DstTableColumnIndex != nil && edge.DstTableColumnIndex == nil {
return fmt.Errorf("other should not have dst column index")
} else if other.DstTableColumnIndex != nil {
edgeColumn := *edge.DstTableColumnIndex
otherColumn := *other.DstTableColumnIndex
if edgeColumn != otherColumn {
return fmt.Errorf("dst column differs: edge=%d, other=%d", edgeColumn, otherColumn)
}
}
return nil
}

View file

@ -17,19 +17,19 @@ func TestSerialization(t *testing.T) {
assert.Nil(t, err)
asserts := func(g *d2graph.Graph) {
a := g.Root.ChildrenArray[0]
a_a := a.ChildrenArray[0]
assert.Equal(t, 4, len(g.Objects))
assert.Equal(t, 1, len(g.Root.ChildrenArray))
assert.Equal(t, 1, len(g.Root.ChildrenArray[0].ChildrenArray))
assert.Equal(t, 2, len(g.Root.ChildrenArray[0].ChildrenArray[0].ChildrenArray))
assert.Equal(t,
g.Root.ChildrenArray[0],
g.Root.ChildrenArray[0].ChildrenArray[0].Parent,
)
assert.Equal(t, 1, len(a.ChildrenArray))
assert.Equal(t, 2, len(a_a.ChildrenArray))
assert.Equal(t, a, a_a.Parent)
assert.Equal(t, g.Root, a.Parent)
assert.Equal(t,
g.Root,
g.Root.ChildrenArray[0].Parent,
)
assert.Contains(t, a.Children, "a")
assert.Contains(t, a_a.Children, "b")
assert.Contains(t, a_a.Children, "c")
assert.Equal(t, 1, len(g.Edges))
assert.Equal(t, "b", g.Edges[0].Src.ID)

252
d2ir/compile.go Normal file
View file

@ -0,0 +1,252 @@
package d2ir
import (
"oss.terrastruct.com/d2/d2ast"
"oss.terrastruct.com/d2/d2parser"
)
type compiler struct {
err d2parser.ParseError
}
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))
}
func Compile(ast *d2ast.Map) (*Map, error) {
c := &compiler{}
m := &Map{}
m.initRoot()
m.parent.(*Field).References[0].Context.Scope = ast
c.compileMap(m, ast)
c.compileScenarios(m)
c.compileSteps(m)
if !c.err.Empty() {
return nil, c.err
}
return m, nil
}
func (c *compiler) compileScenarios(m *Map) {
scenariosf := m.GetField("scenarios")
if scenariosf == nil {
return
}
scenarios := scenariosf.Map()
if scenarios == nil {
return
}
for _, sf := range scenarios.Fields {
if sf.Map() == nil {
continue
}
base := m.CopyBase(sf)
OverlayMap(base, sf.Map())
sf.Composite = base
c.compileScenarios(sf.Map())
c.compileSteps(sf.Map())
}
}
func (c *compiler) compileSteps(m *Map) {
stepsf := m.GetField("steps")
if stepsf == nil {
return
}
steps := stepsf.Map()
if steps == nil {
return
}
for i, sf := range steps.Fields {
if sf.Map() == nil {
continue
}
var base *Map
if i == 0 {
base = m.CopyBase(sf)
} else {
base = steps.Fields[i-1].Map().CopyBase(sf)
}
OverlayMap(base, sf.Map())
sf.Composite = base
c.compileScenarios(sf.Map())
c.compileSteps(sf.Map())
}
}
func (c *compiler) compileMap(dst *Map, ast *d2ast.Map) {
for _, n := range ast.Nodes {
switch {
case n.MapKey != nil:
c.compileKey(&RefContext{
Key: n.MapKey,
Scope: ast,
ScopeMap: dst,
})
case n.Substitution != nil:
panic("TODO")
}
}
}
func (c *compiler) compileKey(refctx *RefContext) {
if len(refctx.Key.Edges) == 0 {
c.compileField(refctx.ScopeMap, refctx.Key.Key, refctx)
} else {
c.compileEdges(refctx)
}
}
func (c *compiler) compileField(dst *Map, kp *d2ast.KeyPath, refctx *RefContext) {
f, err := dst.EnsureField(kp, refctx)
if err != nil {
c.err.Errors = append(c.err.Errors, err.(d2ast.Error))
return
}
if refctx.Key.Primary.Unbox() != nil {
f.Primary_ = &Scalar{
parent: f,
Value: refctx.Key.Primary.Unbox(),
}
}
if refctx.Key.Value.Array != nil {
a := &Array{
parent: f,
}
c.compileArray(a, refctx.Key.Value.Array)
f.Composite = a
} else if refctx.Key.Value.Map != nil {
if f.Map() == nil {
f.Composite = &Map{
parent: f,
}
}
c.compileMap(f.Map(), refctx.Key.Value.Map)
} else if refctx.Key.Value.ScalarBox().Unbox() != nil {
f.Primary_ = &Scalar{
parent: f,
Value: refctx.Key.Value.ScalarBox().Unbox(),
}
}
}
func (c *compiler) compileEdges(refctx *RefContext) {
if refctx.Key.Key != nil {
f, err := refctx.ScopeMap.EnsureField(refctx.Key.Key, refctx)
if err != nil {
c.err.Errors = append(c.err.Errors, err.(d2ast.Error))
return
}
if _, ok := f.Composite.(*Array); ok {
c.errorf(refctx.Key.Key, "cannot index into array")
return
}
if f.Map() == nil {
f.Composite = &Map{
parent: f,
}
}
refctx.ScopeMap = f.Map()
}
eida := NewEdgeIDs(refctx.Key)
for i, eid := range eida {
refctx = refctx.Copy()
refctx.Edge = refctx.Key.Edges[i]
var e *Edge
if eid.Index != nil {
ea := refctx.ScopeMap.GetEdges(eid)
if len(ea) == 0 {
c.errorf(refctx.Edge, "indexed edge does not exist")
continue
}
e = ea[0]
e.References = append(e.References, &EdgeReference{
Context: refctx,
})
refctx.ScopeMap.appendFieldReferences(0, refctx.Edge.Src, refctx)
refctx.ScopeMap.appendFieldReferences(0, refctx.Edge.Dst, refctx)
} else {
_, err := refctx.ScopeMap.EnsureField(refctx.Edge.Src, refctx)
if err != nil {
c.err.Errors = append(c.err.Errors, err.(d2ast.Error))
continue
}
_, err = refctx.ScopeMap.EnsureField(refctx.Edge.Dst, refctx)
if err != nil {
c.err.Errors = append(c.err.Errors, err.(d2ast.Error))
continue
}
e, err = refctx.ScopeMap.CreateEdge(eid, refctx)
if err != nil {
c.err.Errors = append(c.err.Errors, err.(d2ast.Error))
continue
}
}
if refctx.Key.EdgeKey != nil {
if e.Map_ == nil {
e.Map_ = &Map{
parent: e,
}
}
c.compileField(e.Map_, refctx.Key.EdgeKey, refctx)
} else {
if refctx.Key.Primary.Unbox() != nil {
e.Primary_ = &Scalar{
parent: e,
Value: refctx.Key.Primary.Unbox(),
}
}
if refctx.Key.Value.Array != nil {
c.errorf(refctx.Key.Value.Unbox(), "edges cannot be assigned arrays")
continue
} else if refctx.Key.Value.Map != nil {
if e.Map_ == nil {
e.Map_ = &Map{
parent: e,
}
}
c.compileMap(e.Map_, refctx.Key.Value.Map)
} else if refctx.Key.Value.ScalarBox().Unbox() != nil {
e.Primary_ = &Scalar{
parent: e,
Value: refctx.Key.Value.ScalarBox().Unbox(),
}
}
}
}
}
func (c *compiler) compileArray(dst *Array, a *d2ast.Array) {
for _, an := range a.Nodes {
var irv Value
switch v := an.Unbox().(type) {
case *d2ast.Array:
ira := &Array{
parent: dst,
}
c.compileArray(ira, v)
irv = ira
case *d2ast.Map:
irm := &Map{
parent: dst,
}
c.compileMap(irm, v)
irv = irm
case d2ast.Scalar:
irv = &Scalar{
parent: dst,
Value: v,
}
case *d2ast.Substitution:
// panic("TODO")
}
dst.Values = append(dst.Values, irv)
}
}

476
d2ir/compile_test.go Normal file
View file

@ -0,0 +1,476 @@
package d2ir_test
import (
"fmt"
"math/big"
"path/filepath"
"strings"
"testing"
"oss.terrastruct.com/util-go/assert"
"oss.terrastruct.com/util-go/diff"
"oss.terrastruct.com/d2/d2ast"
"oss.terrastruct.com/d2/d2ir"
"oss.terrastruct.com/d2/d2parser"
)
func TestCompile(t *testing.T) {
t.Parallel()
t.Run("fields", testCompileFields)
t.Run("edges", testCompileEdges)
t.Run("layers", testCompileLayers)
t.Run("scenarios", testCompileScenarios)
t.Run("steps", testCompileSteps)
}
type testCase struct {
name string
run func(testing.TB)
}
func runa(t *testing.T, tca []testCase) {
for _, tc := range tca {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
tc.run(t)
})
}
}
func compile(t testing.TB, text string) (*d2ir.Map, error) {
t.Helper()
d2Path := fmt.Sprintf("%v.d2", t.Name())
ast, err := d2parser.Parse(d2Path, strings.NewReader(text), nil)
assert.Success(t, err)
m, err := d2ir.Compile(ast)
if err != nil {
return nil, err
}
err = diff.TestdataJSON(filepath.Join("..", "testdata", "d2ir", t.Name()), m)
if err != nil {
return nil, err
}
return m, nil
}
func assertQuery(t testing.TB, n d2ir.Node, nfields, nedges int, primary interface{}, idStr string) d2ir.Node {
t.Helper()
m := n.Map()
p := n.Primary()
if idStr != "" {
var err error
n, err = m.Query(idStr)
assert.Success(t, err)
assert.NotEqual(t, n, nil)
p = n.Primary()
m = n.Map()
}
assert.Equal(t, nfields, m.FieldCountRecursive())
assert.Equal(t, nedges, m.EdgeCountRecursive())
if !makeScalar(p).Equal(makeScalar(primary)) {
t.Fatalf("expected primary %#v but got %s", primary, p)
}
return n
}
func makeScalar(v interface{}) *d2ir.Scalar {
s := &d2ir.Scalar{}
switch v := v.(type) {
case *d2ir.Scalar:
if v == nil {
s.Value = &d2ast.Null{}
return s
}
return v
case bool:
s.Value = &d2ast.Boolean{
Value: v,
}
case float64:
bv := &big.Rat{}
bv.SetFloat64(v)
s.Value = &d2ast.Number{
Value: bv,
}
case int:
s.Value = &d2ast.Number{
Value: big.NewRat(int64(v), 1),
}
case string:
s.Value = d2ast.FlatDoubleQuotedString(v)
default:
if v != nil {
panic(fmt.Sprintf("d2ir: unexpected type to makeScalar: %#v", v))
}
s.Value = &d2ast.Null{}
}
return s
}
func testCompileFields(t *testing.T) {
t.Parallel()
tca := []testCase{
{
name: "root",
run: func(t testing.TB) {
m, err := compile(t, `x`)
assert.Success(t, err)
assertQuery(t, m, 1, 0, nil, "")
assertQuery(t, m, 0, 0, nil, "x")
},
},
{
name: "label",
run: func(t testing.TB) {
m, err := compile(t, `x: yes`)
assert.Success(t, err)
assertQuery(t, m, 1, 0, nil, "")
assertQuery(t, m, 0, 0, "yes", "x")
},
},
{
name: "nested",
run: func(t testing.TB) {
m, err := compile(t, `x.y: yes`)
assert.Success(t, err)
assertQuery(t, m, 2, 0, nil, "")
assertQuery(t, m, 1, 0, nil, "x")
assertQuery(t, m, 0, 0, "yes", "x.y")
},
},
{
name: "array",
run: func(t testing.TB) {
m, err := compile(t, `x: [1;2;3;4]`)
assert.Success(t, err)
assertQuery(t, m, 1, 0, nil, "")
f := assertQuery(t, m, 0, 0, nil, "x").(*d2ir.Field)
assert.String(t, `[1; 2; 3; 4]`, f.Composite.String())
},
},
{
name: "null",
run: func(t testing.TB) {
m, err := compile(t, `pq: pq
pq: null`)
assert.Success(t, err)
assertQuery(t, m, 1, 0, nil, "")
// null doesn't delete pq from *Map so that for language tooling
// we maintain the references.
// Instead d2compiler will ensure it doesn't get rendered.
assertQuery(t, m, 0, 0, nil, "pq")
},
},
}
runa(t, tca)
t.Run("primary", func(t *testing.T) {
t.Parallel()
tca := []testCase{
{
name: "root",
run: func(t testing.TB) {
m, err := compile(t, `x: yes { pqrs }`)
assert.Success(t, err)
assertQuery(t, m, 2, 0, nil, "")
assertQuery(t, m, 1, 0, "yes", "x")
assertQuery(t, m, 0, 0, nil, "x.pqrs")
},
},
{
name: "nested",
run: func(t testing.TB) {
m, err := compile(t, `x.y: yes { pqrs }`)
assert.Success(t, err)
assertQuery(t, m, 3, 0, nil, "")
assertQuery(t, m, 2, 0, nil, "x")
assertQuery(t, m, 1, 0, "yes", "x.y")
assertQuery(t, m, 0, 0, nil, "x.y.pqrs")
},
},
}
runa(t, tca)
})
}
func testCompileEdges(t *testing.T) {
t.Parallel()
tca := []testCase{
{
name: "root",
run: func(t testing.TB) {
m, err := compile(t, `x -> y`)
assert.Success(t, err)
assertQuery(t, m, 2, 1, nil, "")
assertQuery(t, m, 0, 0, nil, `(x -> y)[0]`)
assertQuery(t, m, 0, 0, nil, "x")
assertQuery(t, m, 0, 0, nil, "y")
},
},
{
name: "nested",
run: func(t testing.TB) {
m, err := compile(t, `x.y -> z.p`)
assert.Success(t, err)
assertQuery(t, m, 4, 1, nil, "")
assertQuery(t, m, 1, 0, nil, "x")
assertQuery(t, m, 0, 0, nil, "x.y")
assertQuery(t, m, 1, 0, nil, "z")
assertQuery(t, m, 0, 0, nil, "z.p")
assertQuery(t, m, 0, 0, nil, "(x.y -> z.p)[0]")
},
},
{
name: "underscore",
run: func(t testing.TB) {
m, err := compile(t, `p: { _.x -> z }`)
assert.Success(t, err)
assertQuery(t, m, 3, 1, nil, "")
assertQuery(t, m, 0, 0, nil, "x")
assertQuery(t, m, 1, 0, nil, "p")
assertQuery(t, m, 0, 0, nil, "(x -> p.z)[0]")
},
},
{
name: "chain",
run: func(t testing.TB) {
m, err := compile(t, `a -> b -> c -> d`)
assert.Success(t, err)
assertQuery(t, m, 4, 3, nil, "")
assertQuery(t, m, 0, 0, nil, "a")
assertQuery(t, m, 0, 0, nil, "b")
assertQuery(t, m, 0, 0, nil, "c")
assertQuery(t, m, 0, 0, nil, "d")
assertQuery(t, m, 0, 0, nil, "(a -> b)[0]")
assertQuery(t, m, 0, 0, nil, "(b -> c)[0]")
assertQuery(t, m, 0, 0, nil, "(c -> d)[0]")
},
},
}
runa(t, tca)
t.Run("errs", func(t *testing.T) {
t.Parallel()
tca := []testCase{
{
name: "bad_edge",
run: func(t testing.TB) {
_, err := compile(t, `(x -> y): { p -> q }`)
assert.ErrorString(t, err, `TestCompile/edges/errs/bad_edge.d2:1:13: cannot create edge inside edge`)
},
},
}
runa(t, tca)
})
}
func testCompileLayers(t *testing.T) {
t.Parallel()
tca := []testCase{
{
name: "root",
run: func(t testing.TB) {
m, err := compile(t, `x -> y
layers: {
bingo: { p.q.z }
}`)
assert.Success(t, err)
assertQuery(t, m, 7, 1, nil, "")
assertQuery(t, m, 0, 0, nil, `(x -> y)[0]`)
assertQuery(t, m, 0, 0, nil, "x")
assertQuery(t, m, 0, 0, nil, "y")
assertQuery(t, m, 3, 0, nil, "layers.bingo")
},
},
}
runa(t, tca)
t.Run("errs", func(t *testing.T) {
t.Parallel()
tca := []testCase{
{
name: "1/bad_edge",
run: func(t testing.TB) {
_, err := compile(t, `layers.x -> layers.y`)
assert.ErrorString(t, err, `TestCompile/layers/errs/1/bad_edge.d2:1:1: cannot create edges between boards`)
},
},
{
name: "2/bad_edge",
run: func(t testing.TB) {
_, err := compile(t, `layers -> scenarios`)
assert.ErrorString(t, err, `TestCompile/layers/errs/2/bad_edge.d2:1:1: edge with board keyword alone doesn't make sense`)
},
},
{
name: "3/bad_edge",
run: func(t testing.TB) {
_, err := compile(t, `layers.x.y -> steps.z.p`)
assert.ErrorString(t, err, `TestCompile/layers/errs/3/bad_edge.d2:1:1: cannot create edges between boards`)
},
},
{
name: "4/good_edge",
run: func(t testing.TB) {
_, err := compile(t, `layers.x.y -> layers.x.y`)
assert.Success(t, err)
},
},
}
runa(t, tca)
})
}
func testCompileScenarios(t *testing.T) {
t.Parallel()
tca := []testCase{
{
name: "root",
run: func(t testing.TB) {
m, err := compile(t, `x -> y
scenarios: {
bingo: { p.q.z }
nuclear: { quiche }
}`)
assert.Success(t, err)
assertQuery(t, m, 13, 3, nil, "")
assertQuery(t, m, 0, 0, nil, "x")
assertQuery(t, m, 0, 0, nil, "y")
assertQuery(t, m, 0, 0, nil, `(x -> y)[0]`)
assertQuery(t, m, 5, 1, nil, "scenarios.bingo")
assertQuery(t, m, 0, 0, nil, "scenarios.bingo.x")
assertQuery(t, m, 0, 0, nil, "scenarios.bingo.y")
assertQuery(t, m, 0, 0, nil, `scenarios.bingo.(x -> y)[0]`)
assertQuery(t, m, 2, 0, nil, "scenarios.bingo.p")
assertQuery(t, m, 1, 0, nil, "scenarios.bingo.p.q")
assertQuery(t, m, 0, 0, nil, "scenarios.bingo.p.q.z")
assertQuery(t, m, 3, 1, nil, "scenarios.nuclear")
assertQuery(t, m, 0, 0, nil, "scenarios.nuclear.x")
assertQuery(t, m, 0, 0, nil, "scenarios.nuclear.y")
assertQuery(t, m, 0, 0, nil, `scenarios.nuclear.(x -> y)[0]`)
assertQuery(t, m, 0, 0, nil, "scenarios.nuclear.quiche")
},
},
}
runa(t, tca)
}
func testCompileSteps(t *testing.T) {
t.Parallel()
tca := []testCase{
{
name: "root",
run: func(t testing.TB) {
m, err := compile(t, `x -> y
steps: {
bingo: { p.q.z }
nuclear: { quiche }
}`)
assert.Success(t, err)
assertQuery(t, m, 16, 3, nil, "")
assertQuery(t, m, 0, 0, nil, "x")
assertQuery(t, m, 0, 0, nil, "y")
assertQuery(t, m, 0, 0, nil, `(x -> y)[0]`)
assertQuery(t, m, 5, 1, nil, "steps.bingo")
assertQuery(t, m, 0, 0, nil, "steps.bingo.x")
assertQuery(t, m, 0, 0, nil, "steps.bingo.y")
assertQuery(t, m, 0, 0, nil, `steps.bingo.(x -> y)[0]`)
assertQuery(t, m, 2, 0, nil, "steps.bingo.p")
assertQuery(t, m, 1, 0, nil, "steps.bingo.p.q")
assertQuery(t, m, 0, 0, nil, "steps.bingo.p.q.z")
assertQuery(t, m, 6, 1, nil, "steps.nuclear")
assertQuery(t, m, 0, 0, nil, "steps.nuclear.x")
assertQuery(t, m, 0, 0, nil, "steps.nuclear.y")
assertQuery(t, m, 0, 0, nil, `steps.nuclear.(x -> y)[0]`)
assertQuery(t, m, 2, 0, nil, "steps.nuclear.p")
assertQuery(t, m, 1, 0, nil, "steps.nuclear.p.q")
assertQuery(t, m, 0, 0, nil, "steps.nuclear.p.q.z")
assertQuery(t, m, 0, 0, nil, "steps.nuclear.quiche")
},
},
{
name: "recursive",
run: func(t testing.TB) {
m, err := compile(t, `x -> y
steps: {
bingo: { p.q.z }
nuclear: {
quiche
scenarios: {
bavarian: {
perseverance
}
}
}
}`)
assert.Success(t, err)
assertQuery(t, m, 25, 4, nil, "")
assertQuery(t, m, 0, 0, nil, "x")
assertQuery(t, m, 0, 0, nil, "y")
assertQuery(t, m, 0, 0, nil, `(x -> y)[0]`)
assertQuery(t, m, 5, 1, nil, "steps.bingo")
assertQuery(t, m, 0, 0, nil, "steps.bingo.x")
assertQuery(t, m, 0, 0, nil, "steps.bingo.y")
assertQuery(t, m, 0, 0, nil, `steps.bingo.(x -> y)[0]`)
assertQuery(t, m, 2, 0, nil, "steps.bingo.p")
assertQuery(t, m, 1, 0, nil, "steps.bingo.p.q")
assertQuery(t, m, 0, 0, nil, "steps.bingo.p.q.z")
assertQuery(t, m, 15, 2, nil, "steps.nuclear")
assertQuery(t, m, 0, 0, nil, "steps.nuclear.x")
assertQuery(t, m, 0, 0, nil, "steps.nuclear.y")
assertQuery(t, m, 0, 0, nil, `steps.nuclear.(x -> y)[0]`)
assertQuery(t, m, 2, 0, nil, "steps.nuclear.p")
assertQuery(t, m, 1, 0, nil, "steps.nuclear.p.q")
assertQuery(t, m, 0, 0, nil, "steps.nuclear.p.q.z")
assertQuery(t, m, 0, 0, nil, "steps.nuclear.quiche")
assertQuery(t, m, 7, 1, nil, "steps.nuclear.scenarios.bavarian")
assertQuery(t, m, 0, 0, nil, "steps.nuclear.scenarios.bavarian.x")
assertQuery(t, m, 0, 0, nil, "steps.nuclear.scenarios.bavarian.y")
assertQuery(t, m, 0, 0, nil, `steps.nuclear.scenarios.bavarian.(x -> y)[0]`)
assertQuery(t, m, 2, 0, nil, "steps.nuclear.scenarios.bavarian.p")
assertQuery(t, m, 1, 0, nil, "steps.nuclear.scenarios.bavarian.p.q")
assertQuery(t, m, 0, 0, nil, "steps.nuclear.scenarios.bavarian.p.q.z")
assertQuery(t, m, 0, 0, nil, "steps.nuclear.scenarios.bavarian.quiche")
assertQuery(t, m, 0, 0, nil, "steps.nuclear.scenarios.bavarian.perseverance")
},
},
}
runa(t, tca)
}

1055
d2ir/d2ir.go Normal file

File diff suppressed because it is too large Load diff

69
d2ir/d2ir_test.go Normal file
View file

@ -0,0 +1,69 @@
package d2ir_test
import (
"testing"
"oss.terrastruct.com/util-go/assert"
"oss.terrastruct.com/d2/d2ast"
"oss.terrastruct.com/d2/d2ir"
)
func TestCopy(t *testing.T) {
t.Parallel()
const scalStr = `Those who claim the dead never return to life haven't ever been around.`
s := &d2ir.Scalar{
Value: d2ast.FlatUnquotedString(scalStr),
}
a := &d2ir.Array{
Values: []d2ir.Value{
&d2ir.Scalar{
Value: &d2ast.Boolean{
Value: true,
},
},
},
}
m2 := &d2ir.Map{
Fields: []*d2ir.Field{
{Primary_: s},
},
}
const keyStr = `Absence makes the heart grow frantic.`
f := &d2ir.Field{
Name: keyStr,
Primary_: s,
Composite: a,
}
e := &d2ir.Edge{
Primary_: s,
Map_: m2,
}
m := &d2ir.Map{
Fields: []*d2ir.Field{f},
Edges: []*d2ir.Edge{e},
}
m = m.Copy(nil).(*d2ir.Map)
f.Name = `Many a wife thinks her husband is the world's greatest lover.`
assert.Equal(t, m, m.Fields[0].Parent())
assert.Equal(t, keyStr, m.Fields[0].Name)
assert.Equal(t, m.Fields[0], m.Fields[0].Primary_.Parent())
assert.Equal(t, m.Fields[0], m.Fields[0].Composite.(*d2ir.Array).Parent())
assert.Equal(t,
m.Fields[0].Composite,
m.Fields[0].Composite.(*d2ir.Array).Values[0].(*d2ir.Scalar).Parent(),
)
assert.Equal(t, m, m.Edges[0].Parent())
assert.Equal(t, m.Edges[0], m.Edges[0].Primary_.Parent())
assert.Equal(t, m.Edges[0], m.Edges[0].Map_.Parent())
assert.Equal(t, m.Edges[0].Map_, m.Edges[0].Map_.Fields[0].Parent())
assert.Equal(t, m.Edges[0].Map_.Fields[0], m.Edges[0].Map_.Fields[0].Primary_.Parent())
}

52
d2ir/merge.go Normal file
View file

@ -0,0 +1,52 @@
package d2ir
func OverlayMap(base, overlay *Map) {
for _, of := range overlay.Fields {
bf := base.GetField(of.Name)
if bf == nil {
base.Fields = append(base.Fields, of.Copy(base).(*Field))
continue
}
OverlayField(bf, of)
}
for _, oe := range overlay.Edges {
bea := base.GetEdges(oe.ID)
if len(bea) == 0 {
base.Edges = append(base.Edges, oe.Copy(base).(*Edge))
continue
}
be := bea[0]
OverlayEdge(be, oe)
}
}
func OverlayField(bf, of *Field) {
if of.Primary_ != nil {
bf.Primary_ = of.Primary_.Copy(bf).(*Scalar)
}
if of.Composite != nil {
if bf.Map() != nil && of.Map() != nil {
OverlayMap(bf.Map(), of.Map())
} else {
bf.Composite = of.Composite.Copy(bf).(*Map)
}
}
bf.References = append(bf.References, of.References...)
}
func OverlayEdge(be, oe *Edge) {
if oe.Primary_ != nil {
be.Primary_ = oe.Primary_.Copy(be).(*Scalar)
}
if oe.Map_ != nil {
if be.Map_ != nil {
OverlayMap(be.Map(), oe.Map_)
} else {
be.Map_ = oe.Map_.Copy(be).(*Map)
}
}
be.References = append(be.References, oe.References...)
}

62
d2ir/query.go Normal file
View file

@ -0,0 +1,62 @@
package d2ir
import (
"fmt"
"oss.terrastruct.com/d2/d2parser"
)
// QueryAll is only for tests and debugging.
func (m *Map) QueryAll(idStr string) (na []Node, _ error) {
k, err := d2parser.ParseMapKey(idStr)
if err != nil {
return nil, err
}
if k.Key != nil {
f := m.GetField(k.Key.IDA()...)
if f == nil {
return nil, nil
}
if len(k.Edges) == 0 {
na = append(na, f)
return na, nil
}
m = f.Map()
if m == nil {
return nil, nil
}
}
eida := NewEdgeIDs(k)
for _, eid := range eida {
ea := m.GetEdges(eid)
for _, e := range ea {
if k.EdgeKey == nil {
na = append(na, e)
} else if e.Map_ != nil {
f := e.Map_.GetField(k.EdgeKey.IDA()...)
if f != nil {
na = append(na, f)
}
}
}
}
return na, nil
}
// Query is only for tests and debugging.
func (m *Map) Query(idStr string) (Node, error) {
na, err := m.QueryAll(idStr)
if err != nil {
return nil, err
}
if len(na) == 0 {
return nil, nil
}
if len(na) > 1 {
return nil, fmt.Errorf("expected only one query result but got: %#v", err)
}
return na[0], nil
}

View file

@ -30,7 +30,10 @@ var setupJS string
//go:embed dagre.js
var dagreJS string
const MIN_SEGMENT_LEN = 10
const (
MIN_SEGMENT_LEN = 10
MIN_RANK_SEP = 60
)
type ConfigurableOpts struct {
NodeSep int `json:"nodesep"`
@ -39,7 +42,7 @@ type ConfigurableOpts struct {
var DefaultOpts = ConfigurableOpts{
NodeSep: 60,
EdgeSep: 40,
EdgeSep: 20,
}
type DagreNode struct {
@ -104,6 +107,25 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
rootAttrs.rankdir = "TB"
}
maxContainerLabelHeight := 0
for _, obj := range g.Objects {
if len(obj.ChildrenArray) == 0 || obj.Parent == g.Root {
continue
}
if obj.LabelHeight != nil {
maxContainerLabelHeight = go2.Max(maxContainerLabelHeight, *obj.LabelHeight+label.PADDING)
}
if obj.Attributes.Icon != nil && obj.Attributes.Shape.Value != d2target.ShapeImage {
contentBox := geo.NewBox(geo.NewPoint(0, 0), float64(obj.Width), float64(obj.Height))
shapeType := d2target.DSL_SHAPE_TO_SHAPE_TYPE[obj.Attributes.Shape.Value]
s := shape.NewShape(shapeType, contentBox)
iconSize := d2target.GetIconSize(s.GetInnerBox(), string(label.InsideTopLeft))
// Since dagre container labels are pushed up, we don't want a child container to collide
maxContainerLabelHeight = go2.Max(maxContainerLabelHeight, (iconSize+label.PADDING*2)*2)
}
}
maxLabelSize := 0
for _, edge := range g.Edges {
size := edge.LabelDimensions.Width
@ -112,7 +134,7 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
}
maxLabelSize = go2.Max(maxLabelSize, size)
}
rootAttrs.ranksep = go2.Max(100, maxLabelSize+40)
rootAttrs.ranksep = go2.Max(go2.Max(100, maxLabelSize+40), maxContainerLabelHeight)
configJS := setGraphAttrs(rootAttrs)
if _, err := vm.RunString(configJS); err != nil {
@ -130,6 +152,9 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
if obj.Attributes.Shape.Value == d2target.ShapeImage || obj.Attributes.Icon != nil {
height += float64(*obj.LabelHeight) + label.PADDING
}
if len(obj.ChildrenArray) > 0 {
height += float64(*obj.LabelHeight) + label.PADDING
}
}
loadScript += generateAddNodeLine(id, int(obj.Width), int(height))
if obj.Parent != g.Root {
@ -191,7 +216,7 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
if obj.LabelWidth != nil && obj.LabelHeight != nil {
if len(obj.ChildrenArray) > 0 {
obj.LabelPosition = go2.Pointer(string(label.InsideTopCenter))
obj.LabelPosition = go2.Pointer(string(label.OutsideTopCenter))
} else if obj.Attributes.Shape.Value == d2target.ShapeImage {
obj.LabelPosition = go2.Pointer(string(label.OutsideBottomCenter))
// remove the extra height we added to the node when passing to dagre
@ -203,7 +228,12 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
}
}
if obj.Attributes.Icon != nil {
obj.IconPosition = go2.Pointer(string(label.InsideMiddleCenter))
if len(obj.ChildrenArray) > 0 {
obj.IconPosition = go2.Pointer(string(label.OutsideTopLeft))
obj.LabelPosition = go2.Pointer(string(label.OutsideTopRight))
} else {
obj.IconPosition = go2.Pointer(string(label.InsideMiddleCenter))
}
}
}
@ -248,6 +278,106 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
}
}
}
points = points[startIndex : endIndex+1]
points[0] = start
points[len(points)-1] = end
edge.Route = points
}
for _, obj := range g.Objects {
if obj.LabelHeight == nil || len(obj.ChildrenArray) <= 0 {
continue
}
// usually you don't want to take away here more than what was added, which is the label height
// however, if the label height is more than the ranksep/2, we'll have no padding around children anymore
// so cap the amount taken off at ranksep/2
subtract := float64(go2.Min(rootAttrs.ranksep/2, *obj.LabelHeight+label.PADDING))
obj.Height -= subtract
// If the edge is connected to two descendants that are about to be downshifted, their whole route gets downshifted
movedEdges := make(map[*d2graph.Edge]struct{})
for _, e := range g.Edges {
currSrc := e.Src
currDst := e.Dst
isSrcDesc := false
isDstDesc := false
for currSrc != nil {
if currSrc == obj {
isSrcDesc = true
break
}
currSrc = currSrc.Parent
}
for currDst != nil {
if currDst == obj {
isDstDesc = true
break
}
currDst = currDst.Parent
}
if isSrcDesc && isDstDesc {
stepSize := subtract
if e.Src != obj || e.Dst != obj {
stepSize /= 2.
}
movedEdges[e] = struct{}{}
for _, p := range e.Route {
p.Y += stepSize
}
}
}
// Downshift descendents and edges that have one endpoint connected to a descendant
q := []*d2graph.Object{obj}
for len(q) > 0 {
curr := q[0]
q = q[1:]
stepSize := subtract
// The object itself needs to move down the height it was just subtracted
// all descendents move half, to maintain vertical padding
if curr != obj {
stepSize /= 2.
}
curr.TopLeft.Y += stepSize
shouldMove := func(p *geo.Point) bool {
if curr != obj {
return true
}
// Edge should only move if it's not connected to the bottom side of the shrinking container
// Give some margin for error
return !(obj.TopLeft.Y+obj.Height-1 <= p.Y && obj.TopLeft.Y+obj.Height+1 >= p.Y && p.X != obj.TopLeft.X && p.X != (obj.TopLeft.X+obj.Width))
}
for _, e := range g.Edges {
if _, ok := movedEdges[e]; ok {
continue
}
if e.Src == curr {
if shouldMove(e.Route[0]) {
e.Route[0].Y += stepSize
}
}
if e.Dst == curr {
if shouldMove(e.Route[len(e.Route)-1]) {
e.Route[len(e.Route)-1].Y += stepSize
}
}
}
for _, c := range curr.ChildrenArray {
q = append(q, c)
}
}
}
for _, edge := range g.Edges {
points := edge.Route
startIndex, endIndex := 0, len(points)-1
start, end := points[startIndex], points[endIndex]
// arrowheads can appear broken if segments are very short from dagre routing a point just outside the shape
// to fix this, we try extending the previous segment into the shape instead of having a very short segment

View file

@ -87,9 +87,9 @@ type ConfigurableOpts struct {
var DefaultOpts = ConfigurableOpts{
Algorithm: "layered",
NodeSpacing: 100.0,
Padding: "[top=75,left=75,bottom=75,right=75]",
EdgeNodeSpacing: 50.0,
NodeSpacing: 70.0,
Padding: "[top=50,left=50,bottom=50,right=50]",
EdgeNodeSpacing: 40.0,
SelfLoopSpacing: 50.0,
}
@ -132,7 +132,7 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
elkGraph := &ELKGraph{
ID: "root",
LayoutOptions: &elkOpts{
Thoroughness: 20,
Thoroughness: 8,
EdgeEdgeBetweenLayersSpacing: 50,
HierarchyHandling: "INCLUDE_CHILDREN",
ConsiderModelOrder: "NODES_AND_EDGES",
@ -188,7 +188,7 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
if len(obj.ChildrenArray) > 0 {
n.LayoutOptions = &elkOpts{
ForceNodeModelOrder: true,
Thoroughness: 20,
Thoroughness: 8,
EdgeEdgeBetweenLayersSpacing: 50,
HierarchyHandling: "INCLUDE_CHILDREN",
ConsiderModelOrder: "NODES_AND_EDGES",
@ -199,6 +199,24 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
Padding: opts.Padding,
},
}
if n.LayoutOptions.Padding == DefaultOpts.Padding {
// Default
paddingTop := 50
if obj.LabelHeight != nil {
paddingTop = go2.Max(paddingTop, *obj.LabelHeight+label.PADDING)
}
if obj.Attributes.Icon != nil && obj.Attributes.Shape.Value != d2target.ShapeImage {
contentBox := geo.NewBox(geo.NewPoint(0, 0), float64(n.Width), float64(n.Height))
shapeType := d2target.DSL_SHAPE_TO_SHAPE_TYPE[obj.Attributes.Shape.Value]
s := shape.NewShape(shapeType, contentBox)
iconSize := d2target.GetIconSize(s.GetInnerBox(), string(label.InsideTopLeft))
paddingTop = go2.Max(paddingTop, iconSize+label.PADDING*2)
}
n.LayoutOptions.Padding = fmt.Sprintf("[top=%d,left=50,bottom=50,right=50]",
paddingTop,
)
}
}
if obj.LabelWidth != nil && obj.LabelHeight != nil {
@ -310,7 +328,12 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
}
}
if obj.Attributes.Icon != nil {
obj.IconPosition = go2.Pointer(string(label.InsideMiddleCenter))
if len(obj.ChildrenArray) > 0 {
obj.IconPosition = go2.Pointer(string(label.InsideTopLeft))
obj.LabelPosition = go2.Pointer(string(label.InsideTopRight))
} else {
obj.IconPosition = go2.Pointer(string(label.InsideMiddleCenter))
}
}
byID[obj.AbsID()] = obj

View file

@ -98,7 +98,7 @@ func WithoutConstantNears(ctx context.Context, g *d2graph.Graph) (nears []*d2gra
nears = append(nears, obj)
g.Objects = append(g.Objects[:i], g.Objects[i+1:]...)
i--
delete(obj.Parent.Children, obj.ID)
delete(obj.Parent.Children, strings.ToLower(obj.ID))
for i := 0; i < len(obj.Parent.ChildrenArray); i++ {
if obj.Parent.ChildrenArray[i] == obj {
obj.Parent.ChildrenArray = append(obj.Parent.ChildrenArray[:i], obj.Parent.ChildrenArray[i+1:]...)

View file

@ -1,21 +1,24 @@
package d2sequence
// leaves at least 25 units of space on the left/right when computing the space required between actors
const HORIZONTAL_PAD = 50.
// units of space on the left/right when computing the space required between actors
const HORIZONTAL_PAD = 40.
// leaves at least 25 units of space on the top/bottom when computing the space required between messages
const VERTICAL_PAD = 50.
// units of space on the top/bottom when computing the space required between messages
// TODO lower
const VERTICAL_PAD = 40.
const MIN_ACTOR_DISTANCE = 250.
const MIN_ACTOR_DISTANCE = 150.
const MIN_ACTOR_WIDTH = 150.
const MIN_ACTOR_WIDTH = 100.
const SELF_MESSAGE_HORIZONTAL_TRAVEL = 100.
const SELF_MESSAGE_HORIZONTAL_TRAVEL = 80.
const GROUP_CONTAINER_PADDING = 24.
const GROUP_CONTAINER_PADDING = 12.
const EDGE_GROUP_LABEL_PADDING = 20.
// min vertical distance between messages
const MIN_MESSAGE_DISTANCE = 80.
const MIN_MESSAGE_DISTANCE = 30.
// default size
const SPAN_BASE_WIDTH = 12.
@ -24,9 +27,9 @@ const SPAN_BASE_WIDTH = 12.
const SPAN_DEPTH_GROWTH_FACTOR = 8.
// when a span has a single messages
const MIN_SPAN_HEIGHT = 80.
const MIN_SPAN_HEIGHT = 30.
const SPAN_MESSAGE_PAD = 16.
const SPAN_MESSAGE_PAD = 10.
const LIFELINE_STROKE_WIDTH int = 2

View file

@ -5,11 +5,12 @@ import (
"sort"
"strings"
"oss.terrastruct.com/util-go/go2"
"oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/d2target"
"oss.terrastruct.com/d2/lib/geo"
"oss.terrastruct.com/d2/lib/label"
"oss.terrastruct.com/util-go/go2"
)
func WithoutSequenceDiagrams(ctx context.Context, g *d2graph.Graph) (map[string]*sequenceDiagram, map[string]int, map[string]int, error) {
@ -113,6 +114,7 @@ func layoutSequenceDiagram(g *d2graph.Graph, obj *d2graph.Object) (*sequenceDiag
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 {

View file

@ -377,7 +377,7 @@ container -> c: edge 1
}
func TestSelfEdges(t *testing.T) {
g := d2graph.NewGraph(nil)
g := d2graph.NewGraph()
g.Root.Attributes.Shape = d2graph.Scalar{Value: d2target.ShapeSequenceDiagram}
n1 := g.Root.EnsureChild([]string{"n1"})
n1.Box = geo.NewBox(nil, 100, 100)
@ -387,7 +387,7 @@ func TestSelfEdges(t *testing.T) {
Src: n1,
Dst: n1,
Index: 0,
Attributes: d2graph.Attributes{
Attributes: &d2graph.Attributes{
Label: d2graph.Scalar{Value: "left to right"},
},
},
@ -407,17 +407,17 @@ func TestSelfEdges(t *testing.T) {
t.Fatalf("route does not end at the same actor, start at %.5f, end at %.5f", route[0].X, route[3].X)
}
if route[3].Y-route[0].Y != d2sequence.MIN_MESSAGE_DISTANCE {
t.Fatalf("expected route height to be %.f5, got %.5f", d2sequence.MIN_MESSAGE_DISTANCE, route[3].Y-route[0].Y)
if route[3].Y-route[0].Y != d2sequence.MIN_MESSAGE_DISTANCE*1.5 {
t.Fatalf("expected route height to be %.5f, got %.5f", d2sequence.MIN_MESSAGE_DISTANCE*1.5, route[3].Y-route[0].Y)
}
}
func TestSequenceToDescendant(t *testing.T) {
g := d2graph.NewGraph(nil)
g := d2graph.NewGraph()
g.Root.Attributes.Shape = d2graph.Scalar{Value: d2target.ShapeSequenceDiagram}
a := g.Root.EnsureChild([]string{"a"})
a.Box = geo.NewBox(nil, 100, 100)
a.Attributes = d2graph.Attributes{
a.Attributes = &d2graph.Attributes{
Shape: d2graph.Scalar{Value: shape.PERSON_TYPE},
}
a_t1 := a.EnsureChild([]string{"t1"})
@ -425,13 +425,15 @@ func TestSequenceToDescendant(t *testing.T) {
g.Edges = []*d2graph.Edge{
{
Src: a,
Dst: a_t1,
Index: 0,
Src: a,
Dst: a_t1,
Index: 0,
Attributes: &d2graph.Attributes{},
}, {
Src: a_t1,
Dst: a,
Index: 0,
Src: a_t1,
Dst: a,
Index: 0,
Attributes: &d2graph.Attributes{},
},
}

View file

@ -105,6 +105,12 @@ func newSequenceDiagram(objects []*d2graph.Object, messages []*d2graph.Edge) *se
sd.objectRank[actor] = rank
if actor.Width < MIN_ACTOR_WIDTH {
dslShape := strings.ToLower(actor.Attributes.Shape.Value)
switch dslShape {
case d2target.ShapePerson, d2target.ShapeOval, d2target.ShapeSquare, d2target.ShapeCircle:
// scale shape up to min width uniformly
actor.Height *= MIN_ACTOR_WIDTH / actor.Width
}
actor.Width = MIN_ACTOR_WIDTH
}
sd.maxActorHeight = math.Max(sd.maxActorHeight, actor.Height)
@ -150,6 +156,7 @@ func newSequenceDiagram(objects []*d2graph.Object, messages []*d2graph.Edge) *se
for _, message := range sd.messages {
sd.verticalIndices[message.AbsID()] = getEdgeEarliestLineNum(message)
// TODO this should not be global yStep, only affect the neighbors
sd.yStep = math.Max(sd.yStep, float64(message.LabelDimensions.Height))
// ensures that long labels, spanning over multiple actors, don't make for large gaps between actors
@ -170,7 +177,6 @@ func newSequenceDiagram(objects []*d2graph.Object, messages []*d2graph.Edge) *se
if _, exists := sd.firstMessage[message.Dst]; !exists {
sd.firstMessage[message.Dst] = message
}
}
sd.yStep += VERTICAL_PAD
@ -203,6 +209,9 @@ func (sd *sequenceDiagram) placeGroups() {
group.ZIndex = GROUP_Z_INDEX
sd.placeGroup(group)
}
for _, group := range sd.groups {
sd.adjustGroupLabel(group)
}
}
func (sd *sequenceDiagram) placeGroup(group *d2graph.Object) {
@ -225,7 +234,7 @@ func (sd *sequenceDiagram) placeGroup(group *d2graph.Object) {
for _, n := range sd.notes {
inGroup := false
for _, ref := range n.References {
curr := ref.UnresolvedScopeObj
curr := ref.ScopeObj
for curr != nil {
if curr == group {
inGroup = true
@ -240,8 +249,8 @@ func (sd *sequenceDiagram) placeGroup(group *d2graph.Object) {
if inGroup {
minX = math.Min(minX, n.TopLeft.X-HORIZONTAL_PAD)
minY = math.Min(minY, n.TopLeft.Y-MIN_MESSAGE_DISTANCE/2.)
maxY = math.Max(maxY, n.TopLeft.Y+n.Height+HORIZONTAL_PAD)
maxX = math.Max(maxX, n.TopLeft.X+n.Width+MIN_MESSAGE_DISTANCE/2.)
maxX = math.Max(maxX, n.TopLeft.X+n.Width+HORIZONTAL_PAD)
maxY = math.Max(maxY, n.TopLeft.Y+n.Height+MIN_MESSAGE_DISTANCE/2.)
}
}
@ -267,6 +276,56 @@ func (sd *sequenceDiagram) placeGroup(group *d2graph.Object) {
)
}
func (sd *sequenceDiagram) adjustGroupLabel(group *d2graph.Object) {
if group.LabelHeight == nil {
return
}
heightAdd := (*group.LabelHeight + EDGE_GROUP_LABEL_PADDING) - GROUP_CONTAINER_PADDING
if heightAdd < 0 {
return
}
group.Height += float64(heightAdd)
// Extend stuff within this group
for _, g := range sd.groups {
if g.TopLeft.Y < group.TopLeft.Y && g.TopLeft.Y+g.Height > group.TopLeft.Y {
g.Height += float64(heightAdd)
}
}
for _, s := range sd.spans {
if s.TopLeft.Y < group.TopLeft.Y && s.TopLeft.Y+s.Height > group.TopLeft.Y {
s.Height += float64(heightAdd)
}
}
// Move stuff down
for _, m := range sd.messages {
if go2.Min(m.Route[0].Y, m.Route[len(m.Route)-1].Y) > group.TopLeft.Y {
for _, p := range m.Route {
p.Y += float64(heightAdd)
}
}
}
for _, s := range sd.spans {
if s.TopLeft.Y > group.TopLeft.Y {
s.TopLeft.Y += float64(heightAdd)
}
}
for _, g := range sd.groups {
if g.TopLeft.Y > group.TopLeft.Y {
g.TopLeft.Y += float64(heightAdd)
}
}
for _, n := range sd.notes {
if n.TopLeft.Y > group.TopLeft.Y {
n.TopLeft.Y += float64(heightAdd)
}
}
}
// placeActors places actors bottom aligned, side by side with centers spaced by sd.actorXStep
func (sd *sequenceDiagram) placeActors() {
centerX := sd.actors[0].Width / 2.
@ -324,7 +383,7 @@ func (sd *sequenceDiagram) addLifelineEdges() {
actorLifelineEnd := actor.Center()
actorLifelineEnd.Y = endY
sd.lifelines = append(sd.lifelines, &d2graph.Edge{
Attributes: d2graph.Attributes{
Attributes: &d2graph.Attributes{
Style: d2graph.Style{
StrokeDash: &d2graph.Scalar{Value: fmt.Sprintf("%d", LIFELINE_STROKE_DASH)},
StrokeWidth: &d2graph.Scalar{Value: fmt.Sprintf("%d", LIFELINE_STROKE_WIDTH)},
@ -348,7 +407,7 @@ func (sd *sequenceDiagram) placeNotes() {
rankToX[sd.objectRank[actor]] = actor.Center().X
}
for i, note := range sd.notes {
for _, note := range sd.notes {
verticalIndex := sd.verticalIndices[note.AbsID()]
y := sd.maxActorHeight + sd.yStep
@ -357,8 +416,10 @@ func (sd *sequenceDiagram) placeNotes() {
y += sd.yStep
}
}
for _, otherNote := range sd.notes[:i] {
y += otherNote.Height + sd.yStep
for _, otherNote := range sd.notes {
if sd.verticalIndices[otherNote.AbsID()] < verticalIndex {
y += otherNote.Height + sd.yStep
}
}
x := rankToX[sd.objectRank[note]] - (note.Width / 2.)
@ -470,12 +531,12 @@ func (sd *sequenceDiagram) routeMessages() error {
if startCenter := getCenter(message.Src); startCenter != nil {
startX = startCenter.X
} else {
return fmt.Errorf("could not find center of %s", message.Src.AbsID())
return fmt.Errorf("could not find center of %s. Is it declared as an actor?", message.Src.ID)
}
if endCenter := getCenter(message.Dst); endCenter != nil {
endX = endCenter.X
} else {
return fmt.Errorf("could not find center of %s", message.Dst.AbsID())
return fmt.Errorf("could not find center of %s. Is it declared as an actor?", message.Dst.ID)
}
isToDescendant := strings.HasPrefix(message.Dst.AbsID(), message.Src.AbsID()+".")
isFromDescendant := strings.HasPrefix(message.Src.AbsID(), message.Dst.AbsID()+".")
@ -493,7 +554,7 @@ func (sd *sequenceDiagram) routeMessages() error {
if isSelfMessage || isToDescendant || isFromDescendant || isToSibling {
midX := startX + SELF_MESSAGE_HORIZONTAL_TRAVEL
endY := startY + MIN_MESSAGE_DISTANCE
endY := startY + MIN_MESSAGE_DISTANCE*1.5
message.Route = []*geo.Point{
geo.NewPoint(startX, startY),
geo.NewPoint(midX, startY),
@ -520,7 +581,7 @@ func (sd *sequenceDiagram) routeMessages() error {
func getCenter(obj *d2graph.Object) *geo.Point {
if obj == nil {
return nil
} else if obj.TopLeft != nil {
} else if obj.Box != nil && obj.Box.TopLeft != nil {
return obj.Center()
}
return getCenter(obj.Parent)

View file

@ -43,32 +43,65 @@ func Compile(ctx context.Context, input string, opts *CompileOptions) (*d2target
return nil, nil, err
}
d, err := compile(ctx, g, opts)
if err != nil {
return nil, nil, err
}
return d, g, nil
}
func compile(ctx context.Context, g *d2graph.Graph, opts *CompileOptions) (*d2target.Diagram, error) {
if len(g.Objects) > 0 {
err = g.SetDimensions(opts.MeasuredTexts, opts.Ruler, opts.FontFamily)
err := g.SetDimensions(opts.MeasuredTexts, opts.Ruler, opts.FontFamily)
if err != nil {
return nil, nil, err
return nil, err
}
coreLayout, err := getLayout(opts)
if err != nil {
return nil, nil, err
return nil, err
}
constantNears := d2near.WithoutConstantNears(ctx, g)
err = d2sequence.Layout(ctx, g, coreLayout)
if err != nil {
return nil, nil, err
return nil, err
}
err = d2near.Layout(ctx, g, constantNears)
if err != nil {
return nil, nil, err
return nil, err
}
}
diagram, err := d2exporter.Export(ctx, g, opts.FontFamily)
return diagram, g, err
d, err := d2exporter.Export(ctx, g, opts.FontFamily)
if err != nil {
return nil, err
}
for _, l := range g.Layers {
ld, err := compile(ctx, l, opts)
if err != nil {
return nil, err
}
d.Layers = append(d.Layers, ld)
}
for _, l := range g.Scenarios {
ld, err := compile(ctx, l, opts)
if err != nil {
return nil, err
}
d.Scenarios = append(d.Scenarios, ld)
}
for _, l := range g.Steps {
ld, err := compile(ctx, l, opts)
if err != nil {
return nil, err
}
d.Steps = append(d.Steps, ld)
}
return d, nil
}
func getLayout(opts *CompileOptions) (func(context.Context, *d2graph.Graph) error, error) {

View file

@ -396,7 +396,6 @@ func Delete(g *d2graph.Graph, key string) (_ *d2graph.Graph, err error) {
if g != g2 {
return g2, nil
}
g = g2
if len(mk.Edges) == 1 {
obj := g.Root
@ -774,13 +773,14 @@ func deleteObject(g *d2graph.Graph, key *d2ast.KeyPath, obj *d2graph.Object) (*d
if len(ref.MapKey.Edges) == 0 {
isSuffix := ref.KeyPathIndex == len(ref.Key.Path)-1
ref.Key.Path = append(ref.Key.Path[:ref.KeyPathIndex], ref.Key.Path[ref.KeyPathIndex+1:]...)
withoutReserved := go2.Filter(ref.Key.Path, func(x *d2ast.StringBox) bool {
_, ok := d2graph.ReservedKeywords[x.Unbox().ScalarString()]
return !ok
withoutSpecial := go2.Filter(ref.Key.Path, func(x *d2ast.StringBox) bool {
_, isReserved := d2graph.ReservedKeywords[x.Unbox().ScalarString()]
isSpecial := isReserved || x.Unbox().ScalarString() == "_"
return !isSpecial
})
if obj.Attributes.Shape.Value == d2target.ShapeSQLTable || obj.Attributes.Shape.Value == d2target.ShapeClass {
ref.MapKey.Value.Map = nil
} else if len(withoutReserved) == 0 {
} else if len(withoutSpecial) == 0 {
hoistRefChildren(g, key, ref)
deleteFromMap(ref.Scope, ref.MapKey)
} else if ref.MapKey.Value.Unbox() == nil &&
@ -1109,7 +1109,7 @@ func move(g *d2graph.Graph, key, newKey string) (*d2graph.Graph, error) {
Key: detachedMK.Key,
MapKey: detachedMK,
Scope: mostNestedRef.Scope,
}, mostNestedRef.UnresolvedScopeObj)
}, mostNestedRef.ScopeObj)
}
}
@ -1168,7 +1168,7 @@ func move(g *d2graph.Graph, key, newKey string) (*d2graph.Graph, error) {
}
ida := d2graph.Key(ref.Key)
resolvedObj, resolvedIDA, err := d2graph.ResolveUnderscoreKey(ida, obj)
resolvedObj, resolvedIDA, err := d2graph.ResolveUnderscoreKey(ida, ref.ScopeObj)
if err != nil {
return nil, err
}
@ -1284,8 +1284,8 @@ func move(g *d2graph.Graph, key, newKey string) (*d2graph.Graph, error) {
// We don't want this to be underscore-resolved scope. We want to ignore underscores
var scopeak []string
if ref.UnresolvedScopeObj != g.Root {
scopek, err := d2parser.ParseKey(ref.UnresolvedScopeObj.AbsID())
if ref.ScopeObj != g.Root {
scopek, err := d2parser.ParseKey(ref.ScopeObj.AbsID())
if err != nil {
return nil, err
}
@ -2067,7 +2067,13 @@ func hasSpace(tag string) bool {
}
func getMostNestedRefs(obj *d2graph.Object) []d2graph.Reference {
most := obj.References[0]
var most d2graph.Reference
for _, ref := range obj.References {
if len(ref.MapKey.Edges) == 0 {
most = ref
break
}
}
for _, ref := range obj.References {
if len(ref.MapKey.Edges) != 0 {
continue
@ -2081,11 +2087,11 @@ func getMostNestedRefs(obj *d2graph.Object) []d2graph.Reference {
if err != nil {
mostKey = &d2ast.KeyPath{}
}
_, resolvedScopeKey, err := d2graph.ResolveUnderscoreKey(d2graph.Key(scopeKey), obj)
_, resolvedScopeKey, err := d2graph.ResolveUnderscoreKey(d2graph.Key(scopeKey), ref.ScopeObj)
if err != nil {
continue
}
_, resolvedMostKey, err := d2graph.ResolveUnderscoreKey(d2graph.Key(mostKey), obj)
_, resolvedMostKey, err := d2graph.ResolveUnderscoreKey(d2graph.Key(mostKey), ref.ScopeObj)
if err != nil {
continue
}

View file

@ -1373,12 +1373,12 @@ more.(ok.q.z -> p.k): "furbling, v.:"
{
name: "complex_edge_1",
text: `a.b.(x -> y).q.z
text: `a.b.(x -> y).style.animated
`,
key: "a.b",
newName: "ooo",
exp: `a.ooo.(x -> y).q.z
exp: `a.ooo.(x -> y).style.animated
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 4 {
@ -1392,12 +1392,12 @@ more.(ok.q.z -> p.k): "furbling, v.:"
{
name: "complex_edge_2",
text: `a.b.(x -> y).q.z
text: `a.b.(x -> y).style.animated
`,
key: "a.b.x",
newName: "papa",
exp: `a.b.(papa -> y).q.z
exp: `a.b.(papa -> y).style.animated
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 4 {
@ -1454,12 +1454,12 @@ more.(ok.q.z -> p.k): "furbling, v.:"
{
name: "arrows_complex",
text: `a.b.(x -- y).q.z
text: `a.b.(x -- y).style.animated
`,
key: "a.b.(x -- y)[0]",
newName: "(x <-> y)[0]",
exp: `a.b.(x <-> y).q.z
exp: `a.b.(x <-> y).style.animated
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 4 {
@ -1755,6 +1755,20 @@ b
assert.JSON(t, 0, len(g.Objects[0].Children))
},
},
{
name: "out_of_newline_container",
text: `"a\n": {
b
}
`,
key: `"a\n".b`,
newKey: `b`,
exp: `"a\n"
b
`,
},
{
name: "partial_slice",
@ -1987,6 +2001,50 @@ c: {
assert.JSON(t, len(g.Objects), 3)
},
},
{
name: "underscore-connection",
text: `a: {
b
_.c.d -> b
}
c: {
d
}
`,
key: `a.b`,
newKey: `c.b`,
exp: `a: {
_.c.d -> _.c.b
}
c: {
d
b
}
`,
},
{
name: "nested-underscore-move-out",
text: `guitar: {
books: {
_._.pipe
}
}
`,
key: `pipe`,
newKey: `guitar.pipe`,
exp: `guitar: {
books
pipe
}
`,
},
{
name: "flat_middle_container",
@ -3025,7 +3083,7 @@ d
if err == nil {
objectsAfter := len(g.Objects)
if objectsBefore != objectsAfter {
println(d2format.Format(g.AST))
t.Log(d2format.Format(g.AST))
return nil, fmt.Errorf("move cannot destroy or create objects: found %d objects before and %d objects after", objectsBefore, objectsAfter)
}
}
@ -3149,6 +3207,41 @@ c -> d
exp: `books: {
_.pipe
}
`,
},
{
name: "only-underscore",
text: `guitar: {
books: {
_._.pipe
}
}
`,
key: `pipe`,
exp: `guitar: {
books
}
`,
},
{
name: "only-underscore-nested",
text: `guitar: {
books: {
_._.pipe: {
a
}
}
}
`,
key: `pipe`,
exp: `guitar: {
books
}
a
`,
},
{
@ -4703,6 +4796,23 @@ x.y.z.w.e.p.l -> x.y.z.1.2.3.4
"x.x": "x"
}`,
},
{
name: "only-reserved",
text: `guitar: {
books: {
_._.pipe: {
a
}
}
}
`,
key: `pipe`,
exp: `{
"pipe.a": "a"
}`,
},
{
name: "delete_container_with_conflicts",

View file

@ -45,7 +45,7 @@ func Parse(path string, r io.RuneReader, opts *ParseOptions) (*d2ast.Map, error)
}
m := p.parseMap(true)
if !p.err.empty() {
if !p.err.Empty() {
return m, p.err
}
return m, nil
@ -57,7 +57,7 @@ func ParseKey(key string) (*d2ast.KeyPath, error) {
}
k := p.parseKey()
if !p.err.empty() {
if !p.err.Empty() {
return nil, fmt.Errorf("failed to parse key %q: %w", key, p.err)
}
if k == nil {
@ -72,7 +72,7 @@ func ParseMapKey(mapKey string) (*d2ast.Key, error) {
}
mk := p.parseMapKey()
if !p.err.empty() {
if !p.err.Empty() {
return nil, fmt.Errorf("failed to parse map key %q: %w", mapKey, p.err)
}
if mk == nil {
@ -87,7 +87,7 @@ func ParseValue(value string) (d2ast.Value, error) {
}
v := p.parseValue()
if !p.err.empty() {
if !p.err.Empty() {
return nil, fmt.Errorf("failed to parse value %q: %w", value, p.err)
}
if v.Unbox() == nil {
@ -130,7 +130,16 @@ type ParseError struct {
Errors []d2ast.Error `json:"errs"`
}
func (pe ParseError) empty() bool {
func Errorf(n d2ast.Node, f string, v ...interface{}) error {
f = "%v: " + f
v = append([]interface{}{n.GetRange()}, v...)
return d2ast.Error{
Range: n.GetRange(),
Message: fmt.Sprintf(f, v...),
}
}
func (pe ParseError) Empty() bool {
return pe.IOError == nil && len(pe.Errors) == 0
}
@ -138,11 +147,12 @@ func (pe ParseError) Error() string {
var sb strings.Builder
if pe.IOError != nil {
sb.WriteString(pe.IOError.Error())
sb.WriteByte('\n')
}
for _, err := range pe.Errors {
for i, err := range pe.Errors {
if pe.IOError != nil || i > 0 {
sb.WriteByte('\n')
}
sb.WriteString(err.Error())
sb.WriteByte('\n')
}
return sb.String()
}

View file

@ -119,6 +119,9 @@ func (p *execPlugin) Info(ctx context.Context) (_ *PluginInfo, err error) {
return nil, fmt.Errorf("failed to unmarshal json: %w", err)
}
info.Type = "binary"
info.Path = p.path
p.info = &info
return &info, nil
}

View file

@ -71,7 +71,8 @@ type PluginInfo struct {
ShortHelp string `json:"shortHelp"`
LongHelp string `json:"longHelp"`
// These two are set by ListPlugins and not the plugin itself.
// Set to bundled when returning from the plugin.
// execPlugin will set to binary when used.
// bundled | binary
Type string `json:"type"`
// If Type == binary then this contains the absolute path to the binary.
@ -122,12 +123,6 @@ func ListPluginInfos(ctx context.Context, ps []Plugin) ([]*PluginInfo, error) {
if err != nil {
return nil, err
}
if ep, ok := p.(*execPlugin); ok {
info.Type = "binary"
info.Path = ep.path
} else {
info.Type = "bundled"
}
infoSlice = append(infoSlice, info)
}

View file

@ -66,11 +66,12 @@ func (p dagrePlugin) Info(ctx context.Context) (*PluginInfo, error) {
return &PluginInfo{
Name: "dagre",
Type: "bundled",
ShortHelp: "The directed graph layout library Dagre",
LongHelp: fmt.Sprintf(`dagre is a directed graph layout library for JavaScript.
See https://github.com/dagrejs/dagre
See https://d2lang.com/tour/dagre for more.
Flags correspond to ones found at https://github.com/dagrejs/dagre/wiki. See dagre's reference for more on each.
Flags correspond to ones found at https://github.com/dagrejs/dagre/wiki.
Flags:
%s

View file

@ -86,12 +86,13 @@ func (p elkPlugin) Info(ctx context.Context) (*PluginInfo, error) {
}
return &PluginInfo{
Name: "elk",
Type: "bundled",
ShortHelp: "Eclipse Layout Kernel (ELK) with the Layered algorithm.",
LongHelp: fmt.Sprintf(`ELK is a layout engine offered by Eclipse.
Originally written in Java, it has been ported to Javascript and cross-compiled into D2.
See https://github.com/kieler/elkjs for more.
See https://d2lang.com/tour/elk for more.
Flags correspond to ones found at https://www.eclipse.org/elk/reference.html. See ELK's reference for more on each.
Flags correspond to ones found at https://www.eclipse.org/elk/reference.html.
Flags:
%s

View file

@ -14,7 +14,6 @@ import (
tassert "github.com/stretchr/testify/assert"
"oss.terrastruct.com/util-go/assert"
"oss.terrastruct.com/util-go/diff"
"oss.terrastruct.com/util-go/go2"
"oss.terrastruct.com/d2/d2layouts/d2dagrelayout"
@ -49,6 +48,132 @@ func TestSketch(t *testing.T) {
script: `a -> b: hello
`,
},
{
name: "crows feet",
script: `a1 <-> b1: {
style.stroke-width: 1
source-arrowhead: {
shape: cf-many
}
target-arrowhead: {
shape: cf-many
}
}
a2 <-> b2: {
style.stroke-width: 3
source-arrowhead: {
shape: cf-many
}
target-arrowhead: {
shape: cf-many
}
}
a3 <-> b3: {
style.stroke-width: 6
source-arrowhead: {
shape: cf-many
}
target-arrowhead: {
shape: cf-many
}
}
c1 <-> d1: {
style.stroke-width: 1
source-arrowhead: {
shape: cf-many-required
}
target-arrowhead: {
shape: cf-many-required
}
}
c2 <-> d2: {
style.stroke-width: 3
source-arrowhead: {
shape: cf-many-required
}
target-arrowhead: {
shape: cf-many-required
}
}
c3 <-> d3: {
style.stroke-width: 6
source-arrowhead: {
shape: cf-many-required
}
target-arrowhead: {
shape: cf-many-required
}
}
e1 <-> f1: {
style.stroke-width: 1
source-arrowhead: {
shape: cf-one
}
target-arrowhead: {
shape: cf-one
}
}
e2 <-> f2: {
style.stroke-width: 3
source-arrowhead: {
shape: cf-one
}
target-arrowhead: {
shape: cf-one
}
}
e3 <-> f3: {
style.stroke-width: 6
source-arrowhead: {
shape: cf-one
}
target-arrowhead: {
shape: cf-one
}
}
g1 <-> h1: {
style.stroke-width: 1
source-arrowhead: {
shape: cf-one-required
}
target-arrowhead: {
shape: cf-one-required
}
}
g2 <-> h2: {
style.stroke-width: 3
source-arrowhead: {
shape: cf-one-required
}
target-arrowhead: {
shape: cf-one-required
}
}
g3 <-> h3: {
style.stroke-width: 6
source-arrowhead: {
shape: cf-one-required
}
target-arrowhead: {
shape: cf-one-required
}
}
c <-> d <-> f: {
style.stroke-width: 1
style.stroke: "orange"
source-arrowhead: {
shape: cf-many-required
}
target-arrowhead: {
shape: cf-one
}
}
`,
},
{
name: "twitter",
script: `timeline mixer: "" {
@ -63,7 +188,7 @@ func TestSketch(t *testing.T) {
}
People discovery: "People discovery \nservice"
admixer: Ad mixer {
fill: "#c1a2f3"
style.fill: "#c1a2f3"
}
onboarding service: "Onboarding \nservice"
@ -107,7 +232,7 @@ Android: {
web -> twitter fe
timeline scorer: "Timeline\nScorer" {
fill: "#ffdef1"
style.fill "#ffdef1"
}
home ranker: Home Ranker
@ -119,7 +244,7 @@ timeline mixer -> home ranker: {
}
timeline mixer -> timeline service
home mixer: Home mixer {
# fill: "#c1a2f3"
# style.fill "#c1a2f3"
}
container0.graphql -> home mixer: {
style.stroke-dash: 4
@ -146,7 +271,7 @@ prediction service2: Prediction Service {
icon: https://cdn-icons-png.flaticon.com/512/6461/6461819.png
}
home scorer: Home Scorer {
fill: "#ffdef1"
style.fill "#ffdef1"
}
manhattan: Manhattan
memcache: Memcache {
@ -154,15 +279,15 @@ memcache: Memcache {
}
fetch: Fetch {
multiple: true
style.multiple: true
shape: step
}
feature: Feature {
multiple: true
style.multiple: true
shape: step
}
scoring: Scoring {
multiple: true
style.multiple: true
shape: step
}
fetch -> feature
@ -433,7 +558,4 @@ func run(t *testing.T, tc testCase) {
var xmlParsed interface{}
err = xml.Unmarshal(svgBytes, &xmlParsed)
assert.Success(t, err)
err = diff.Testdata(filepath.Join(dataPath, "sketch"), ".svg", svgBytes)
assert.Success(t, err)
}

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 302 KiB

After

Width:  |  Height:  |  Size: 386 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 284 KiB

After

Width:  |  Height:  |  Size: 305 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 334 KiB

After

Width:  |  Height:  |  Size: 405 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 228 KiB

After

Width:  |  Height:  |  Size: 241 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 280 KiB

After

Width:  |  Height:  |  Size: 296 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 227 KiB

After

Width:  |  Height:  |  Size: 241 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 277 KiB

After

Width:  |  Height:  |  Size: 291 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 301 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 340 KiB

After

Width:  |  Height:  |  Size: 357 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 115 KiB

After

Width:  |  Height:  |  Size: 148 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 418 KiB

After

Width:  |  Height:  |  Size: 513 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 807 KiB

After

Width:  |  Height:  |  Size: 807 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 655 KiB

After

Width:  |  Height:  |  Size: 655 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 654 KiB

After

Width:  |  Height:  |  Size: 654 KiB

View file

@ -44,7 +44,7 @@ const (
appendixIconRadius = 16
)
var multipleOffset = geo.NewVector(10, -10)
var multipleOffset = geo.NewVector(d2target.MULTIPLE_OFFSET, -d2target.MULTIPLE_OFFSET)
//go:embed tooltip.svg
var TooltipIcon string
@ -65,21 +65,14 @@ type RenderOpts struct {
DarkThemeID int64
}
func dimensions(writer io.Writer, diagram *d2target.Diagram, pad int) (width, height int, topLeft, bottomRight d2target.Point) {
func dimensions(diagram *d2target.Diagram, pad int) (left, top, width, height int) {
tl, br := diagram.BoundingBox()
w := br.X - tl.X + pad*2
h := br.Y - tl.Y + pad*2
left = tl.X - pad
top = tl.Y - pad
width = br.X - tl.X + pad*2
height = br.Y - tl.Y + pad*2
outTl := d2target.Point{
X: tl.X - pad,
Y: tl.Y - pad,
}
outBr := d2target.Point{
X: br.X - pad,
Y: br.Y - pad,
}
return w, h, outTl, outBr
return left, top, width, height
}
func arrowheadMarkerID(isTarget bool, connection d2target.Connection) string {
@ -118,8 +111,8 @@ func arrowheadDimensions(arrowhead d2target.Arrowhead, strokeWidth float64) (wid
widthMultiplier = 12
heightMultiplier = 12
case d2target.CfOne, d2target.CfMany, d2target.CfOneRequired, d2target.CfManyRequired:
widthMultiplier = 14
heightMultiplier = 15
widthMultiplier = 9
heightMultiplier = 9
}
clippedStrokeWidth := go2.Max(MIN_ARROWHEAD_STROKE_WIDTH, strokeWidth)
@ -279,7 +272,7 @@ func arrowheadMarker(isTarget bool, id string, bgColor string, connection d2targ
path = circleEl.Render()
case d2target.CfOne, d2target.CfMany, d2target.CfOneRequired, d2target.CfManyRequired:
offset := 4.0 + float64(connection.StrokeWidth*2)
offset := 3.0 + float64(connection.StrokeWidth)*1.8
var modifierEl *svgstyle.ThemableElement
if arrowhead == d2target.CfOneRequired || arrowhead == d2target.CfManyRequired {
@ -294,7 +287,7 @@ func arrowheadMarker(isTarget bool, id string, bgColor string, connection d2targ
modifierEl.Attributes = fmt.Sprintf(`stroke-width="%d"`, connection.StrokeWidth)
} else {
modifierEl = svgstyle.NewThemableElement("circle")
modifierEl.Cx = offset/2.0 + 1.0
modifierEl.Cx = offset/2.0 + 2.0
modifierEl.Cy = height / 2.0
modifierEl.R = offset / 2.0
modifierEl.Fill = bgColor
@ -308,17 +301,17 @@ func arrowheadMarker(isTarget bool, id string, bgColor string, connection d2targ
childPathEl.D = fmt.Sprintf("M%f,%f %f,%f M%f,%f %f,%f M%f,%f %f,%f",
width-3.0, height/2.0,
width+offset, height/2.0,
offset+2.0, height/2.0,
offset+3.0, height/2.0,
width+offset, 0.,
offset+2.0, height/2.0,
offset+3.0, height/2.0,
width+offset, height,
)
} else {
childPathEl.D = fmt.Sprintf("M%f,%f %f,%f M%f,%f %f,%f",
width-3.0, height/2.0,
width+offset, height/2.0,
offset*1.8, 0.,
offset*1.8, height,
offset*2.0, 0.,
offset*2.0, height,
)
}
@ -792,7 +785,7 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske
var multipleTL *geo.Point
if targetShape.Multiple {
multipleTL = tl.AddVector(geo.NewVector(d2target.MULTIPLE_OFFSET, -d2target.MULTIPLE_OFFSET))
multipleTL = tl.AddVector(multipleOffset)
}
switch targetShape.Type {
@ -807,7 +800,7 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske
drawClass(writer, targetShape)
}
addAppendixItems(writer, targetShape)
fmt.Fprintf(writer, `</g>`)
fmt.Fprint(writer, `</g>`)
fmt.Fprint(writer, closingTag)
return labelMask, nil
case d2target.ShapeSQLTable:
@ -821,7 +814,7 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske
drawTable(writer, targetShape)
}
addAppendixItems(writer, targetShape)
fmt.Fprintf(writer, `</g>`)
fmt.Fprint(writer, `</g>`)
fmt.Fprint(writer, closingTag)
return labelMask, nil
case d2target.ShapeOval:
@ -834,7 +827,7 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske
if err != nil {
return "", err
}
fmt.Fprintf(writer, out)
fmt.Fprint(writer, out)
} else {
fmt.Fprint(writer, renderDoubleOval(tl, width, height, fill, stroke, style))
}
@ -847,7 +840,7 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske
if err != nil {
return "", err
}
fmt.Fprintf(writer, out)
fmt.Fprint(writer, out)
} else {
fmt.Fprint(writer, renderOval(tl, width, height, fill, stroke, style))
}
@ -867,6 +860,11 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske
// TODO should standardize "" to rectangle
case d2target.ShapeRectangle, d2target.ShapeSequenceDiagram, "":
// TODO use Rx property of NewThemableElement instead
rx := ""
if targetShape.BorderRadius != 0 {
rx = fmt.Sprintf(` rx="%d"`, targetShape.BorderRadius)
}
if targetShape.ThreeDee {
fmt.Fprint(writer, render3dRect(targetShape))
} else {
@ -880,6 +878,7 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske
el.Fill = fill
el.Stroke = stroke
el.Style = style
el.Attributes = rx
fmt.Fprint(writer, el.Render())
}
if sketchRunner != nil {
@ -897,6 +896,7 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske
el.Fill = fill
el.Stroke = stroke
el.Style = style
el.Attributes = rx
fmt.Fprint(writer, el.Render())
}
} else {
@ -909,6 +909,7 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske
el.Fill = fill
el.Stroke = stroke
el.Style = style
el.Attributes = rx
fmt.Fprint(writer, el.Render())
el = svgstyle.NewThemableElement("rect")
@ -919,6 +920,7 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske
el.Fill = fill
el.Stroke = stroke
el.Style = style
el.Attributes = rx
fmt.Fprint(writer, el.Render())
}
if sketchRunner != nil {
@ -936,6 +938,7 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske
el.Fill = fill
el.Stroke = stroke
el.Style = style
el.Attributes = rx
fmt.Fprint(writer, el.Render())
el = svgstyle.NewThemableElement("rect")
@ -946,6 +949,7 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske
el.Fill = fill
el.Stroke = stroke
el.Style = style
el.Attributes = rx
fmt.Fprint(writer, el.Render())
}
}
@ -982,8 +986,13 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske
}
}
// to examine GetInsidePlacement
// padX, padY := s.GetDefaultPadding()
// innerTL := s.GetInsidePlacement(s.GetInnerBox().Width, s.GetInnerBox().Height, padX, padY)
// fmt.Fprint(writer, renderOval(&innerTL, 5, 5, "fill:red;"))
// Closes the class=shape
fmt.Fprintf(writer, `</g>`)
fmt.Fprint(writer, `</g>`)
if targetShape.Icon != nil && targetShape.Type != d2target.ShapeImage {
iconPosition := label.Position(targetShape.IconPosition)
@ -993,7 +1002,7 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske
} else {
box = s.GetInnerBox()
}
iconSize := targetShape.GetIconSize(box)
iconSize := d2target.GetIconSize(box, targetShape.IconPosition)
tl := iconPosition.GetPointOnBox(box, label.PADDING, float64(iconSize), float64(iconSize))
@ -1014,7 +1023,10 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske
} else {
box = s.GetInnerBox()
}
labelTL := labelPosition.GetPointOnBox(box, label.PADDING, float64(targetShape.LabelWidth), float64(targetShape.LabelHeight))
labelTL := labelPosition.GetPointOnBox(box, label.PADDING,
float64(targetShape.LabelWidth),
float64(targetShape.LabelHeight),
)
fontClass := "text"
if targetShape.Bold {
@ -1054,7 +1066,7 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske
rectEl.Style = fmt.Sprintf(`fill:%s`, style.Get(chroma.Background).Background.String())
fmt.Fprint(writer, rectEl.Render())
// Padding
fmt.Fprintf(writer, `<g transform="translate(6 6)">`)
fmt.Fprint(writer, `<g transform="translate(6 6)">`)
for index, tokens := range chroma.SplitTokensIntoLines(iterator.Tokens()) {
// TODO mono font looks better with 1.2 em (use px equivalent), but textmeasure needs to account for it. Not obvious how that should be done
@ -1069,7 +1081,7 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske
}
fmt.Fprint(writer, "</text>")
}
fmt.Fprintf(writer, "</g></g>")
fmt.Fprint(writer, "</g></g>")
} else if targetShape.Type == d2target.ShapeText && targetShape.Language == "latex" {
render, err := d2latex.Render(targetShape.Label)
if err != nil {
@ -1102,6 +1114,15 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske
if targetShape.Color != color.Empty {
fontColor = targetShape.Color
}
if targetShape.LabelFill != "" {
rectEl := svgstyle.NewThemableElement("rect")
rectEl.X = labelTL.X
rectEl.Y = labelTL.Y
rectEl.Width = float64(targetShape.LabelWidth)
rectEl.Height = float64(targetShape.LabelHeight)
rectEl.Fill = targetShape.LabelFill
fmt.Fprint(writer, rectEl.Render())
}
textEl := svgstyle.NewThemableElement("text")
textEl.X = labelTL.X + float64(targetShape.LabelWidth)/2
// text is vertically positioned at its baseline which is at labelTL+FontSize
@ -1132,7 +1153,7 @@ func addAppendixItems(writer io.Writer, shape d2target.Shape) {
shape.Pos.Y-appendixIconRadius,
TooltipIcon,
)
fmt.Fprintf(writer, `<title>%s</title>`, shape.Tooltip)
fmt.Fprintf(writer, `<title>%s</title>`, svg.EscapeText(shape.Tooltip))
}
if shape.Link != "" {
@ -1525,13 +1546,13 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) {
}
// Note: we always want this since we reference it on connections even if there end up being no masked labels
w, h, tl, _ := dimensions(buf, diagram, pad)
left, top, w, h := dimensions(diagram, pad)
fmt.Fprint(buf, strings.Join([]string{
fmt.Sprintf(`<mask id="%s" maskUnits="userSpaceOnUse" x="%d" y="%d" width="%d" height="%d">`,
labelMaskID, -pad, -pad, w, h,
labelMaskID, left, top, w, h,
),
fmt.Sprintf(`<rect x="%d" y="%d" width="%d" height="%d" fill="white"></rect>`,
-pad, -pad, w, h,
left, top, w, h,
),
strings.Join(labelMasks, "\n"),
`</mask>`,
@ -1540,8 +1561,8 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) {
// TODO minify
// TODO background stuff. e.g. dotted, grid, colors
backgroundEl := svgstyle.NewThemableElement("rect")
backgroundEl.X = float64(tl.X)
backgroundEl.Y = float64(tl.Y)
backgroundEl.X = float64(left)
backgroundEl.Y = float64(top)
backgroundEl.Width = float64(w)
backgroundEl.Height = float64(h)
backgroundEl.Fill = color.N7
@ -1572,7 +1593,7 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) {
// render the document
docRendered := fmt.Sprintf(`<?xml version="1.0" encoding="utf-8"?><svg id="d2-svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="%d" height="%d" viewBox="%d %d %d %d">%s%s%s</svg>`,
w, h, tl.X-pad, tl.Y-pad, w, h,
w, h, left, top, w, h,
svgOut,
backgroundEl.Render(),
buf.String(),

View file

@ -63,8 +63,8 @@ func TestDarkTheme(t *testing.T) {
}
People discovery: "People discovery \nservice"
admixer: Ad mixer {
fill: "#cba6f7"
font-color: "#000000"
style.fill: "#cba6f7"
style.font-color: "#000000"
}
onboarding service: "Onboarding \nservice"
@ -108,8 +108,8 @@ Android: {
web -> twitter fe
timeline scorer: "Timeline\nScorer" {
fill: "#fab387"
font-color: "#000000"
style.fill: "#fab387"
style.font-color: "#000000"
}
home ranker: Home Ranker
@ -121,7 +121,7 @@ timeline mixer -> home ranker: {
}
timeline mixer -> timeline service
home mixer: Home mixer {
# fill: "#c1a2f3"
# style.fill: "#c1a2f3"
}
container0.graphql -> home mixer: {
style.stroke-dash: 4
@ -148,8 +148,8 @@ prediction service2: Prediction Service {
icon: https://cdn-icons-png.flaticon.com/512/6461/6461819.png
}
home scorer: Home Scorer {
fill: "#eba0ac"
font-color: "#000000"
style.fill: "#eba0ac"
style.font-color: "#000000"
}
manhattan: Manhattan
memcache: Memcache {
@ -157,15 +157,15 @@ memcache: Memcache {
}
fetch: Fetch {
multiple: true
style.multiple: true
shape: step
}
feature: Feature {
multiple: true
style.multiple: true
shape: step
}
scoring: Scoring {
multiple: true
style.multiple: true
shape: step
}
fetch -> feature

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 197 KiB

After

Width:  |  Height:  |  Size: 196 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 240 KiB

After

Width:  |  Height:  |  Size: 240 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 253 KiB

After

Width:  |  Height:  |  Size: 253 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 188 KiB

After

Width:  |  Height:  |  Size: 188 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 238 KiB

After

Width:  |  Height:  |  Size: 238 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 188 KiB

After

Width:  |  Height:  |  Size: 188 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 238 KiB

After

Width:  |  Height:  |  Size: 238 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 299 KiB

After

Width:  |  Height:  |  Size: 299 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 188 KiB

After

Width:  |  Height:  |  Size: 188 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 64 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 320 KiB

After

Width:  |  Height:  |  Size: 320 KiB

View file

@ -2,13 +2,14 @@ package d2target
import (
"fmt"
"oss.terrastruct.com/d2/d2renderers/d2fonts"
)
const (
PrefixPadding = 10
PrefixWidth = 20
CenterPadding = 50
// 10px of padding top and bottom so text doesn't look squished
VerticalPadding = 20
)
type Class struct {
@ -22,10 +23,10 @@ type ClassField struct {
Visibility string `json:"visibility"`
}
func (cf ClassField) Text() *MText {
func (cf ClassField) Text(fontSize int) *MText {
return &MText{
Text: fmt.Sprintf("%s%s", cf.Name, cf.Type),
FontSize: d2fonts.FONT_SIZE_L,
FontSize: fontSize,
IsBold: false,
IsItalic: false,
Shape: "class",
@ -49,10 +50,10 @@ type ClassMethod struct {
Visibility string `json:"visibility"`
}
func (cm ClassMethod) Text() *MText {
func (cm ClassMethod) Text(fontSize int) *MText {
return &MText{
Text: fmt.Sprintf("%s%s", cm.Name, cm.Return),
FontSize: d2fonts.FONT_SIZE_L,
FontSize: fontSize,
IsBold: false,
IsItalic: false,
Shape: "class",

View file

@ -37,6 +37,10 @@ type Diagram struct {
Shapes []Shape `json:"shapes"`
Connections []Connection `json:"connections"`
Layers []*Diagram `json:"layers,omitempty"`
Scenarios []*Diagram `json:"scenarios,omitempty"`
Steps []*Diagram `json:"steps,omitempty"`
}
func (diagram Diagram) HashID() (string, error) {
@ -185,9 +189,6 @@ func (s Shape) CSSStyle() string {
dashSize, gapSize := svg.GetStrokeDashAttributes(float64(s.StrokeWidth), s.StrokeDash)
out += fmt.Sprintf(`stroke-dasharray:%f,%f;`, dashSize, gapSize)
}
if s.BorderRadius != 0 {
out += fmt.Sprintf(`rx:%d;`, s.BorderRadius)
}
return out
}
@ -222,8 +223,9 @@ type Text struct {
Bold bool `json:"bold"`
Underline bool `json:"underline"`
LabelWidth int `json:"labelWidth"`
LabelHeight int `json:"labelHeight"`
LabelWidth int `json:"labelWidth"`
LabelHeight int `json:"labelHeight"`
LabelFill string `json:"labelFill,omitempty"`
}
func BaseShape() *Shape {
@ -525,8 +527,8 @@ func init() {
}
}
func (s *Shape) GetIconSize(box *geo.Box) int {
iconPosition := label.Position(s.IconPosition)
func GetIconSize(box *geo.Box, position string) int {
iconPosition := label.Position(position)
minDimension := int(math.Min(box.Width, box.Height))
halfMinDimension := int(math.Ceil(0.5 * float64(minDimension)))
@ -536,19 +538,19 @@ func (s *Shape) GetIconSize(box *geo.Box) int {
if iconPosition == label.InsideMiddleCenter {
size = halfMinDimension
} else {
size = go2.IntMin(
size = go2.Min(
minDimension,
go2.IntMax(DEFAULT_ICON_SIZE, halfMinDimension),
go2.Max(DEFAULT_ICON_SIZE, halfMinDimension),
)
}
size = go2.IntMin(size, MAX_ICON_SIZE)
size = go2.Min(size, MAX_ICON_SIZE)
if !iconPosition.IsOutside() {
size = go2.IntMin(size,
go2.IntMin(
go2.IntMax(int(box.Width)-2*label.PADDING, 0),
go2.IntMax(int(box.Height)-2*label.PADDING, 0),
size = go2.Min(size,
go2.Min(
go2.Max(int(box.Width)-2*label.PADDING, 0),
go2.Max(int(box.Height)-2*label.PADDING, 0),
),
)
}

View file

@ -1,11 +1,13 @@
package d2target
import "oss.terrastruct.com/d2/d2renderers/d2fonts"
const (
NamePadding = 10
TypePadding = 20
HeaderPadding = 20
HeaderPadding = 10
// Setting table font size sets it for columns
// The header needs to be a little larger for visual hierarchy
HeaderFontAdd = 4
)
type SQLTable struct {
@ -19,18 +21,18 @@ type SQLColumn struct {
Reference string `json:"reference"`
}
func (c SQLColumn) Texts() []*MText {
func (c SQLColumn) Texts(fontSize int) []*MText {
return []*MText{
{
Text: c.Name.Label,
FontSize: d2fonts.FONT_SIZE_L,
FontSize: fontSize,
IsBold: false,
IsItalic: false,
Shape: "sql_table",
},
{
Text: c.Type.Label,
FontSize: d2fonts.FONT_SIZE_L,
FontSize: fontSize,
IsBold: false,
IsItalic: false,
Shape: "sql_table",

View file

@ -10,7 +10,7 @@ timeline mixer: "" {
}
People discovery: "People discovery \nservice"
admixer: Ad mixer {
fill: "#c1a2f3"
style.fill: "#c1a2f3"
}
onboarding service: "Onboarding \nservice"
@ -54,7 +54,7 @@ Android: {
web -> twitter fe
timeline scorer: "Timeline\nScorer" {
fill: "#ffdef1"
style.fill: "#ffdef1"
}
home ranker: Home Ranker
@ -66,7 +66,7 @@ timeline mixer -> home ranker: {
}
timeline mixer -> timeline service
home mixer: Home mixer {
# fill: "#c1a2f3"
# style.fill: "#c1a2f3"
}
container0.graphql -> home mixer: {
style.stroke-dash: 4
@ -93,7 +93,7 @@ prediction service2: Prediction Service {
icon: https://cdn-icons-png.flaticon.com/512/6461/6461819.png
}
home scorer: Home Scorer {
fill: "#ffdef1"
style.fill: "#ffdef1"
}
manhattan: Manhattan
memcache: Memcache {
@ -101,15 +101,16 @@ memcache: Memcache {
}
fetch: Fetch {
multiple: true
style.multiple: true
shape: step
}
feature: Feature {
multiple: true
style.multiple: true
shape: step
}
scoring: Scoring {
multiple: true
style.multiple: true
shape: step
}
fetch -> feature

View file

@ -17,6 +17,6 @@ If a change results in test diffs, you can run this script to generate a visual
report with the old vs new renders.
```
go run ./e2etests/report/main.go
go run ./e2etests/report/main.go -delta
open ./e2etests/out/e2e_report.html
```

View file

@ -12,7 +12,7 @@ import (
"cdr.dev/slog"
tassert "github.com/stretchr/testify/assert"
trequire "github.com/stretchr/testify/require"
"oss.terrastruct.com/util-go/assert"
"oss.terrastruct.com/util-go/diff"
@ -38,6 +38,7 @@ func TestE2E(t *testing.T) {
t.Run("regression", testRegression)
t.Run("todo", testTodo)
t.Run("measured", testMeasured)
t.Run("unicode", testUnicode)
}
func testSanity(t *testing.T) {
@ -77,6 +78,7 @@ type testCase struct {
mtexts []*d2target.MText
assertions func(t *testing.T, diagram *d2target.Diagram)
skip bool
expErr string
}
func runa(t *testing.T, tcs []testCase) {
@ -101,18 +103,19 @@ func serde(t *testing.T, tc testCase, ruler *textmeasure.Ruler) {
g, err := d2compiler.Compile("", strings.NewReader(tc.script), &d2compiler.CompileOptions{
UTF16: false,
})
tassert.Nil(t, err)
trequire.Nil(t, err)
if len(g.Objects) > 0 {
err = g.SetDimensions(nil, ruler, nil)
tassert.Nil(t, err)
trequire.Nil(t, err)
d2near.WithoutConstantNears(ctx, g)
d2sequence.WithoutSequenceDiagrams(ctx, g)
}
b, err := d2graph.SerializeGraph(g)
tassert.Nil(t, err)
trequire.Nil(t, err)
var newG d2graph.Graph
err = d2graph.DeserializeGraph(b, &newG)
tassert.Nil(t, err)
trequire.Nil(t, err)
trequire.Nil(t, d2graph.CompareSerializedGraph(g, &newG))
}
func run(t *testing.T, tc testCase) {
@ -124,9 +127,7 @@ func run(t *testing.T, tc testCase) {
var err error
if tc.mtexts == nil {
ruler, err = textmeasure.NewRuler()
if !tassert.Nil(t, err) {
return
}
trequire.Nil(t, err)
serde(t, tc, ruler)
}
@ -149,8 +150,13 @@ func run(t *testing.T, tc testCase) {
MeasuredTexts: tc.mtexts,
Layout: layout,
})
if !tassert.Nil(t, err) {
if tc.expErr != "" {
assert.Error(t, err)
assert.ErrorString(t, err, tc.expErr)
return
} else {
assert.Success(t, err)
}
if tc.assertions != nil {
@ -172,26 +178,19 @@ func run(t *testing.T, tc testCase) {
assert.Success(t, err)
err = ioutil.WriteFile(pathGotSVG, svgBytes, 0600)
assert.Success(t, err)
// if running from e2ereport.sh, we want to keep .got.svg on a failure
forReport := os.Getenv("E2E_REPORT") != ""
if !forReport {
defer os.Remove(pathGotSVG)
}
// Check that it's valid SVG
var xmlParsed interface{}
err = xml.Unmarshal(svgBytes, &xmlParsed)
assert.Success(t, err)
var err2 error
err = diff.TestdataJSON(filepath.Join(dataPath, "board"), diagram)
assert.Success(t, err)
if os.Getenv("SKIP_SVG_CHECK") == "" {
err = diff.Testdata(filepath.Join(dataPath, "sketch"), ".svg", svgBytes)
assert.Success(t, err)
}
if forReport {
os.Remove(pathGotSVG)
err2 = diff.Testdata(filepath.Join(dataPath, "sketch"), ".svg", svgBytes)
}
assert.Success(t, err)
assert.Success(t, err2)
}
}

View file

@ -444,6 +444,77 @@ group 11: {
}
b -> c
`,
},
{
name: "empty_class_height",
script: `
class1: class with rows {
shape: class
-num: int
-timeout: int
}
class2: class without rows {
shape: class
}
`,
},
{
name: "just-width",
script: `x: "teamwork: having someone to blame" {
width: 100
}
`,
},
{
name: "sequence-panic",
script: `
shape: sequence_diagram
a
group: {
inner_group: {
a -> b
}
}
`,
expErr: "could not find center of b. Is it declared as an actor?",
},
{
name: "ampersand-escape",
script: `h&y: & {
tooltip: beans & rice
}
&foo
&&bar
`,
},
{
name: "dagre-disconnect",
script: `a: {
k.t -> f.i
f.g -> _.s.n
}
k
k.s <-> u.o
h.m.s -> a.f.g
a.f.j -> u.s.j
u: {
c -> _.s.z.c
}
s: {
n: {
style.stroke: red
f
}
}
s.n -> y.r: {style.stroke-width: 8; style.stroke: red}
y.r -> a.g.i: 1\n2\n3\n4
`,
},
}

View file

@ -6,6 +6,7 @@ import (
"flag"
"fmt"
"io/ioutil"
stdlog "log"
"os"
"os/exec"
"path/filepath"
@ -38,6 +39,7 @@ func main() {
flag.BoolVar(&deltaFlag, "delta", false, "Generate the report only for cases that changed.")
flag.StringVar(&testSetFlag, "test-set", "", "Only run set of tests matching this string. e.g. regressions")
flag.StringVar(&testCaseFlag, "test-case", "", "Only run tests matching this string. e.g. all_shapes")
skipTests := flag.Bool("skip-tests", false, "Skip running tests first")
flag.BoolVar(&vFlag, "v", false, "verbose")
flag.Parse()
@ -52,18 +54,20 @@ func main() {
testDir = "./e2etests"
}
ctx := log.Stderr(context.Background())
ctx, cancel := context.WithTimeout(ctx, 2*time.Minute)
defer cancel()
cmd := exec.CommandContext(ctx, "go", "test", testDir, "-run", testMatchString, vString)
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env, "FORCE_COLOR=1")
cmd.Env = append(cmd.Env, "DEBUG=1")
cmd.Env = append(cmd.Env, "TEST_MODE=on")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
log.Debug(ctx, cmd.String())
_ = cmd.Run()
if !*skipTests {
ctx := log.Stderr(context.Background())
ctx, cancel := context.WithTimeout(ctx, 2*time.Minute)
defer cancel()
cmd := exec.CommandContext(ctx, "go", "test", testDir, "-run", testMatchString, vString)
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env, "FORCE_COLOR=1")
cmd.Env = append(cmd.Env, "DEBUG=1")
cmd.Env = append(cmd.Env, "TEST_MODE=on")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
log.Debug(ctx, cmd.String())
_ = cmd.Run()
}
var tests []TestItem
err := filepath.Walk(filepath.Join(testDir, "testdata"), func(path string, info os.FileInfo, err error) error {
@ -96,7 +100,11 @@ func main() {
}
if matchTestSet && matchTestCase {
fullPath := filepath.Join(path, testFile.Name())
absPath, err := filepath.Abs(path)
if err != nil {
stdlog.Fatal(err)
}
fullPath := filepath.Join(absPath, testFile.Name())
hasGot := false
gotPath := strings.Replace(fullPath, "exp.svg", "got.svg", 1)
if _, err := os.Stat(gotPath); err == nil {
@ -139,16 +147,42 @@ func main() {
panic(err)
}
tmplData := TemplateData{
Tests: tests,
}
path := os.Getenv("REPORT_OUTPUT")
if path == "" {
path = filepath.Join(testDir, "./out/e2e_report.html")
}
err = os.MkdirAll(filepath.Dir(path), 0755)
if err != nil {
stdlog.Fatal(err)
}
f, err := os.Create(path)
if err != nil {
panic(fmt.Errorf("error creating file `%s`. %v", path, err))
}
if err := tmpl.Execute(f, tmplData); err != nil {
absReportDir, err := filepath.Abs(filepath.Dir(path))
if err != nil {
stdlog.Fatal(err)
}
// get the test path relative to the report
reportRelPath := func(testPath string) string {
relTestPath, err := filepath.Rel(absReportDir, testPath)
if err != nil {
stdlog.Fatal(err)
}
return relTestPath
}
// update test paths to be relative to report file
for i := range tests {
testItem := &tests[i]
testItem.GotSVG = reportRelPath(testItem.GotSVG)
if testItem.ExpSVG != nil {
*testItem.ExpSVG = reportRelPath(*testItem.ExpSVG)
}
}
if err := tmpl.Execute(f, TemplateData{Tests: tests}); err != nil {
panic(err)
}
}

View file

@ -1,16 +1,26 @@
<html>
<title>E2E report</title>
<body>
<h1>Table of Contents</h1>
<ul>
{{range .Tests}}
<li><a href="#{{.Name}}">{{.Name}}</a></li>
{{end}}
</ul>
<div class="cases">
{{range .Tests}}
<div class="case">
<h1><a href="../{{.GotSVG}}">{{.Name}}</a></h1>
<div class="case" id="{{.Name}}">
<h1><a href="{{.GotSVG}}">{{.Name}}</a></h1>
{{ if .ExpSVG }}
<h2>Expected</h2>
<img src="../{{.ExpSVG}}" width="100%" />
<h2>Got</h2>
<h2 class="case-exp">Expected</h2>
{{ end }}
<img src="../{{.GotSVG}}" width="100%" />
<h2 class="case-got">Got</h2>
<div class="case-img-wrapper">
{{ if .ExpSVG }}
<img src="{{.ExpSVG}}" width="100%" />
{{ end }}
<img src="{{.GotSVG}}" width="100%" />
</div>
</div>
{{end}}
</div>
@ -20,17 +30,24 @@
flex-wrap: wrap;
}
.case {
align-items: center;
justify-content: center;
position: relative;
padding: 20px;
width: 100%;
}
.case svg {
width: 400px;
height: 400px;
.case-img-wrapper {
display: flex;
align-items: center;
}
.case pre {
font-size: 10pt;
width: 400px;
.case img {
width: 600px;
}
.case-exp + .case-got {
position: absolute;
left: 600px;
}
.case h2 {
margin: 0;
display: inline;
}
</style>
</body>

View file

@ -79,23 +79,23 @@ callout -> stored_data -> person
diamond -> oval -> circle
hexagon -> cloud
rectangle.multiple: true
square.multiple: true
page.multiple: true
parallelogram.multiple: true
document.multiple: true
cylinder.multiple: true
queue.multiple: true
package.multiple: true
step.multiple: true
callout.multiple: true
stored_data.multiple: true
person.multiple: true
diamond.multiple: true
oval.multiple: true
circle.multiple: true
hexagon.multiple: true
cloud.multiple: true
rectangle.style.multiple: true
square.style.multiple: true
page.style.multiple: true
parallelogram.style.multiple: true
document.style.multiple: true
cylinder.style.multiple: true
queue.style.multiple: true
package.style.multiple: true
step.style.multiple: true
callout.style.multiple: true
stored_data.style.multiple: true
person.style.multiple: true
diamond.style.multiple: true
oval.style.multiple: true
circle.style.multiple: true
hexagon.style.multiple: true
cloud.style.multiple: true
`,
},
{
@ -126,23 +126,23 @@ callout -> stored_data -> person
diamond -> oval -> circle
hexagon -> cloud
rectangle.shadow: true
square.shadow: true
page.shadow: true
parallelogram.shadow: true
document.shadow: true
cylinder.shadow: true
queue.shadow: true
package.shadow: true
step.shadow: true
callout.shadow: true
stored_data.shadow: true
person.shadow: true
diamond.shadow: true
oval.shadow: true
circle.shadow: true
hexagon.shadow: true
cloud.shadow: true
rectangle.style.shadow: true
square.style.shadow: true
page.style.shadow: true
parallelogram.style.shadow: true
document.style.shadow: true
cylinder.style.shadow: true
queue.style.shadow: true
package.style.shadow: true
step.style.shadow: true
callout.style.shadow: true
stored_data.style.shadow: true
person.style.shadow: true
diamond.style.shadow: true
oval.style.shadow: true
circle.style.shadow: true
hexagon.style.shadow: true
cloud.style.shadow: true
`,
},
{
@ -153,8 +153,8 @@ square: {shape: "square"}
rectangle -> square
rectangle.3d: true
square.3d: true
rectangle.style.3d: true
square.style.3d: true
`,
},
{
@ -165,9 +165,9 @@ d -> g.e -> f -> g -> d.h
},
{
name: "one_three_one_container",
script: `top.start -> a
top.start -> b
top.start -> c
script: `top2.start -> a
top2.start -> b
top2.start -> c
a -> bottom.end
b -> bottom.end
c -> bottom.end
@ -609,10 +609,24 @@ x -> hey -> y
`,
},
{
name: "child_parent_edges",
script: `a.b -> a
a.b -> a.b.c
a.b.c.d -> a.b`,
name: "font_sizes_containers_large",
script: `
ninety nine: {
style.font-size: 99
sixty four: {
style.font-size: 64
thirty two:{
style.font-size: 32
sixteen: {
style.font-size: 16
eight: {
style.font-size: 8
}
}
}
}
}
`,
},
{
name: "lone_h1",
@ -881,6 +895,37 @@ b: {
icon: https://icons.terrastruct.com/essentials/004-picture.svg
}
a -> b
`,
},
{
name: "icon-containers",
script: `vpc: VPC 1 10.1.0.0./16 {
icon: https://icons.terrastruct.com/aws%2F_Group%20Icons%2FVirtual-private-cloud-VPC_light-bg.svg
style: {
stroke: green
font-color: green
fill: white
}
az: Availability Zone A {
style: {
stroke: blue
font-color: blue
stroke-dash: 3
fill: white
}
firewall: Firewall Subnet A {
icon: https://icons.terrastruct.com/aws%2FNetworking%20&%20Content%20Delivery%2FAmazon-Route-53_Hosted-Zone_light-bg.svg
style: {
stroke: purple
font-color: purple
fill: "#e1d5e7"
}
ec2: EC2 Instance {
icon: https://icons.terrastruct.com/aws%2FCompute%2F_Instance%2FAmazon-EC2_C4-Instance_light-bg.svg
}
}
}
}
`,
},
{
@ -1109,17 +1154,17 @@ scorer.t -> itemOutcome.t3: setFeedback(missingConcepts)`,
script: `shape: sequence_diagram
scorer: {
stroke: red
stroke-width: 5
style.stroke: red
style.stroke-width: 5
}
scorer.abc: {
fill: yellow
stroke-width: 7
style.fill: yellow
style.stroke-width: 7
}
scorer -> itemResponse.a: {
stroke-width: 10
style.stroke-width: 10
}
itemResponse.a -> item.a.b
item.a.b -> essayRubric.a.b.c
@ -1531,13 +1576,13 @@ container: {
icon: https://icons.terrastruct.com/essentials/004-picture.svg
}
left: {
left2: {
root: {
shape: image
icon: https://icons.terrastruct.com/essentials/004-picture.svg
}
inner: {
left: {
left2: {
shape: image
icon: https://icons.terrastruct.com/essentials/004-picture.svg
}
@ -1546,8 +1591,8 @@ container: {
icon: https://icons.terrastruct.com/essentials/004-picture.svg
}
}
root -> inner.left: {
label: to inner left
root -> inner.left2: {
label: to inner left2
}
root -> inner.right: {
label: to inner right
@ -1560,7 +1605,7 @@ container: {
icon: https://icons.terrastruct.com/essentials/004-picture.svg
}
inner: {
left: {
left2: {
shape: image
icon: https://icons.terrastruct.com/essentials/004-picture.svg
}
@ -1569,16 +1614,16 @@ container: {
icon: https://icons.terrastruct.com/essentials/004-picture.svg
}
}
root -> inner.left: {
label: to inner left
root -> inner.left2: {
label: to inner left2
}
root -> inner.right: {
label: to inner right
}
}
root -> left.root: {
label: to left container root
root -> left2.root: {
label: to left2 container root
}
root -> right.root: {
@ -1727,9 +1772,8 @@ package.height: 512
{
name: "crow_foot_arrowhead",
script: `
a <-> b: {
style.stroke-width: 3
style.stroke: "#20222a"
a1 <-> b1: {
style.stroke-width: 1
source-arrowhead: {
shape: cf-many
}
@ -1737,32 +1781,120 @@ a <-> b: {
shape: cf-many
}
}
c <--> d <-> f: {
a2 <-> b2: {
style.stroke-width: 3
source-arrowhead: {
shape: cf-many
}
target-arrowhead: {
shape: cf-many
}
}
a3 <-> b3: {
style.stroke-width: 6
source-arrowhead: {
shape: cf-many
}
target-arrowhead: {
shape: cf-many
}
}
c1 <-> d1: {
style.stroke-width: 1
source-arrowhead: {
shape: cf-many-required
}
target-arrowhead: {
shape: cf-many-required
}
}
c2 <-> d2: {
style.stroke-width: 3
source-arrowhead: {
shape: cf-many-required
}
target-arrowhead: {
shape: cf-many-required
}
}
c3 <-> d3: {
style.stroke-width: 6
source-arrowhead: {
shape: cf-many-required
}
target-arrowhead: {
shape: cf-many-required
}
}
e1 <-> f1: {
style.stroke-width: 1
source-arrowhead: {
shape: cf-one
}
target-arrowhead: {
shape: cf-one
}
}
e2 <-> f2: {
style.stroke-width: 3
source-arrowhead: {
shape: cf-one
}
target-arrowhead: {
shape: cf-one
}
}
e3 <-> f3: {
style.stroke-width: 6
source-arrowhead: {
shape: cf-one
}
target-arrowhead: {
shape: cf-one
}
}
g1 <-> h1: {
style.stroke-width: 1
source-arrowhead: {
shape: cf-one-required
}
target-arrowhead: {
shape: cf-one-required
}
}
g2 <-> h2: {
style.stroke-width: 3
source-arrowhead: {
shape: cf-one-required
}
target-arrowhead: {
shape: cf-one-required
}
}
g3 <-> h3: {
style.stroke-width: 6
source-arrowhead: {
shape: cf-one-required
}
target-arrowhead: {
shape: cf-one-required
}
}
c <-> d <-> f: {
style.stroke-width: 1
style.stroke: "orange"
source-arrowhead: {
shape: cf-many-required
}
target-arrowhead: {
shape: cf-many-required
}
}
g <--> h: {
source-arrowhead: {
shape: cf-one
}
target-arrowhead: {
shape: cf-one
}
}
e <--> f: {
source-arrowhead: {
shape: cf-one-required
}
target-arrowhead: {
shape: cf-one-required
}
}`,
`,
},
{
name: "circle_arrowhead",
@ -1838,6 +1970,55 @@ x.y -> a.b: {
style.animated: true
target-arrowhead.shape: cf-many
}
`,
},
{
name: "sql_table_column_styles",
script: `Humor in the Court: {
shape: sql_table
Could you see him from where you were standing?: "I could see his head."
And where was his head?: Just above his shoulders.
style.fill: red
style.stroke: lightgray
style.font-color: orange
style.font-size: 20
}
Humor in the Court2: {
shape: sql_table
Could you see him from where you were standing?: "I could see his head."
And where was his head?: Just above his shoulders.
style.fill: red
style.stroke: lightgray
style.font-color: orange
style.font-size: 30
}
manager: BatchManager {
shape: class
style.font-size: 20
-num: int
-timeout: int
-pid
+getStatus(): Enum
+getJobs(): "Job[]"
+setTimeout(seconds int)
}
manager2: BatchManager {
shape: class
style.font-size: 30
-num: int
-timeout: int
-pid
+getStatus(): Enum
+getJobs(): "Job[]"
+setTimeout(seconds int)
}
`,
},
{
@ -1863,6 +2044,18 @@ x: {
y: {
style.border-radius: 10
}
multiple2: {
style.border-radius: 6
style.multiple: true
}
double: {
style.border-radius: 6
style.double-border: true
}
three-dee: {
style.border-radius: 6
style.3d: true
}
`,
},
{
@ -1877,6 +2070,132 @@ a.sp1 -> a.sp2: redirect
a.sp2 -> b: bar
`,
},
{
name: "people",
script: `
a.shape: person
b.shape: person
c.shape: person
d.shape: person
e.shape: person
f.shape: person
g.shape: person
a: -
b: --
c: ----
d: --------
e: ----------------
f: --------------------------------
g: ----------------------------------------------------------------
1.shape: person
2.shape: person
3.shape: person
4.shape: person
5.shape: person
1.width: 16
2.width: 64
3.width: 128
4.width: 512
# entering both width and height overrides aspect ratio limit
5.height: 256
5.width: 32
`,
},
{
name: "ovals",
script: `
a.shape: oval
b.shape: oval
c.shape: oval
d.shape: oval
e.shape: oval
f.shape: oval
g.shape: oval
a: -
b: --
c: ----
d: --------
e: ----------------
f: --------------------------------
g: ----------------------------------------------------------------
1.shape: oval
2.shape: oval
3.shape: oval
4.shape: oval
5.shape: oval
1.width: 16
2.width: 64
3.width: 128
4.width: 512
# entering both width and height overrides aspect ratio limit
5.height: 256
5.width: 32
`,
},
{
name: "complex-layers",
script: `
desc: Multi-layer diagram of a home.
window: {
style.double-border: true
}
roof
garage
layers: {
window: {
blinds
glass
}
roof: {
shingles
starlink
utility hookup
}
garage: {
tools
vehicles
}
repair: {
desc: How to repair a home.
steps: {
1: {
find contractors: {
craigslist
facebook
}
}
2: {
find contractors -> solicit quotes
}
3: {
obtain quotes -> negotiate
}
4: {
negotiate -> book the best bid
}
}
}
}
scenarios: {
storm: {
water
rain
thunder
}
}`,
},
}
runa(t, tcs)

View file

@ -10,7 +10,7 @@
"y": 0
},
"width": 112,
"height": 12,
"height": 44,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><svg id="d2-svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="316" height="216" viewBox="-202 -202 316 216"><style type="text/css"><![CDATA[.shape {
<?xml version="1.0" encoding="utf-8"?><svg id="d2-svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="316" height="248" viewBox="-102 -102 316 248"><style type="text/css"><![CDATA[.shape {
shape-rendering: geometricPrecision;
stroke-linejoin: round;
}
@ -30,7 +30,7 @@
svgEl.setAttribute("height", height * ratio - 16);
}
});
]]></script><style type="text/css"><![CDATA[]]></style><rect x="-102.000000" y="-102.000000" width="316.000000" height="216.000000" class=" fill-N7" /><g id="a"><g class="shape" ><rect x="0.000000" y="0.000000" width="112.000000" height="12.000000" class=" stroke-N1 fill-N7" style="stroke-width:2;" /><rect x="0.000000" y="0.000000" width="112.000000" height="12.000000" class="class_header fill-N1" /><line x1="0.000000" x2="112.000000" y1="12.000000" y2="12.000000" class=" stroke-N1" style="stroke-width:1" /></g></g><mask id="735671140" maskUnits="userSpaceOnUse" x="-100" y="-100" width="316" height="216">
<rect x="-100" y="-100" width="316" height="216" fill="white"></rect>
]]></script><style type="text/css"><![CDATA[]]></style><rect x="-102.000000" y="-102.000000" width="316.000000" height="248.000000" class=" fill-N7" /><g id="a"><g class="shape" ><rect x="0.000000" y="0.000000" width="112.000000" height="44.000000" class=" stroke-N1 fill-N7" style="stroke-width:2;" /><rect x="0.000000" y="0.000000" width="112.000000" height="44.000000" class="class_header fill-N1" /><line x1="0.000000" x2="112.000000" y1="44.000000" y2="44.000000" class=" stroke-N1" style="stroke-width:1" /></g></g><mask id="148127623" maskUnits="userSpaceOnUse" x="-102" y="-102" width="316" height="248">
<rect x="-102" y="-102" width="316" height="248" fill="white"></rect>
</mask></svg>

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

View file

@ -4,7 +4,7 @@
"shapes": [
{
"id": "a",
"type": "",
"type": "rectangle",
"pos": {
"x": 0,
"y": 0

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><svg id="d2-svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="304" height="304" viewBox="-202 -202 304 304"><style type="text/css"><![CDATA[.shape {
<?xml version="1.0" encoding="utf-8"?><svg id="d2-svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="304" height="304" viewBox="-102 -102 304 304"><style type="text/css"><![CDATA[.shape {
shape-rendering: geometricPrecision;
stroke-linejoin: round;
}
@ -30,7 +30,7 @@
svgEl.setAttribute("height", height * ratio - 16);
}
});
]]></script><style type="text/css"><![CDATA[]]></style><rect x="-102.000000" y="-102.000000" width="304.000000" height="304.000000" class=" fill-N7" /><g id="a"><g class="shape" ><rect x="0.000000" y="0.000000" width="100.000000" height="100.000000" class=" stroke-B1 fill-B6" style="stroke-width:2;" /></g></g><mask id="43897558" maskUnits="userSpaceOnUse" x="-100" y="-100" width="304" height="304">
<rect x="-100" y="-100" width="304" height="304" fill="white"></rect>
]]></script><style type="text/css"><![CDATA[]]></style><rect x="-102.000000" y="-102.000000" width="304.000000" height="304.000000" class=" fill-N7" /><g id="a"><g class="shape" ><rect x="0.000000" y="0.000000" width="100.000000" height="100.000000" class=" stroke-B1 fill-B6" style="stroke-width:2;" /></g></g><mask id="198791073" maskUnits="userSpaceOnUse" x="-102" y="-102" width="304" height="304">
<rect x="-102" y="-102" width="304" height="304" fill="white"></rect>
</mask></svg>

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><svg id="d2-svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="254" height="216" viewBox="-202 -202 254 216"><style type="text/css"><![CDATA[.shape {
<?xml version="1.0" encoding="utf-8"?><svg id="d2-svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="254" height="216" viewBox="-102 -102 254 216"><style type="text/css"><![CDATA[.shape {
shape-rendering: geometricPrecision;
stroke-linejoin: round;
}
@ -30,7 +30,7 @@
svgEl.setAttribute("height", height * ratio - 16);
}
});
]]></script><style type="text/css"><![CDATA[]]></style><rect x="-102.000000" y="-102.000000" width="254.000000" height="216.000000" class=" fill-N7" /><g id="a"><g class="shape" ><rect x="0.000000" y="0.000000" width="50.000000" height="12.000000" class="shape stroke-N1 fill-N7" style="stroke-width:2;" /><rect x="0.000000" y="0.000000" width="50.000000" height="12.000000" class="class_header fill-N1" /></g></g><mask id="2388684491" maskUnits="userSpaceOnUse" x="-100" y="-100" width="254" height="216">
<rect x="-100" y="-100" width="254" height="216" fill="white"></rect>
]]></script><style type="text/css"><![CDATA[]]></style><rect x="-102.000000" y="-102.000000" width="254.000000" height="216.000000" class=" fill-N7" /><g id="a"><g class="shape" ><rect x="0.000000" y="0.000000" width="50.000000" height="12.000000" class="shape stroke-N1 fill-N7" style="stroke-width:2;" /><rect x="0.000000" y="0.000000" width="50.000000" height="12.000000" class="class_header fill-N1" /></g></g><mask id="2388684491" maskUnits="userSpaceOnUse" x="-102" y="-102" width="254" height="216">
<rect x="-102" y="-102" width="254" height="216" fill="white"></rect>
</mask></svg>

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

View file

@ -0,0 +1,130 @@
{
"name": "",
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "h&y",
"type": "rectangle",
"pos": {
"x": 0,
"y": 0
},
"width": 98,
"height": 66,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"borderRadius": 0,
"fill": "B6",
"stroke": "B1",
"shadow": false,
"3d": false,
"multiple": false,
"double-border": false,
"tooltip": "beans & rice",
"link": "",
"icon": null,
"iconPosition": "",
"blend": false,
"fields": null,
"methods": null,
"columns": null,
"label": "&∈",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "N1",
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 21,
"labelHeight": 21,
"labelPosition": "INSIDE_MIDDLE_CENTER",
"zIndex": 0,
"level": 1
},
{
"id": "foo",
"type": "rectangle",
"pos": {
"x": 158,
"y": 0
},
"width": 69,
"height": 66,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"borderRadius": 0,
"fill": "B6",
"stroke": "B1",
"shadow": false,
"3d": false,
"multiple": false,
"double-border": false,
"tooltip": "",
"link": "",
"icon": null,
"iconPosition": "",
"blend": false,
"fields": null,
"methods": null,
"columns": null,
"label": "foo",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "N1",
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 24,
"labelHeight": 21,
"labelPosition": "INSIDE_MIDDLE_CENTER",
"zIndex": 0,
"level": 1
},
{
"id": "\"&bar\"",
"type": "rectangle",
"pos": {
"x": 287,
"y": 0
},
"width": 81,
"height": 66,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"borderRadius": 0,
"fill": "B6",
"stroke": "B1",
"shadow": false,
"3d": false,
"multiple": false,
"double-border": false,
"tooltip": "",
"link": "",
"icon": null,
"iconPosition": "",
"blend": false,
"fields": null,
"methods": null,
"columns": null,
"label": "&bar",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "N1",
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 36,
"labelHeight": 21,
"labelPosition": "INSIDE_MIDDLE_CENTER",
"zIndex": 0,
"level": 1
}
],
"connections": []
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 330 KiB

View file

@ -0,0 +1,130 @@
{
"name": "",
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "h&y",
"type": "rectangle",
"pos": {
"x": 12,
"y": 12
},
"width": 98,
"height": 66,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"borderRadius": 0,
"fill": "B6",
"stroke": "B1",
"shadow": false,
"3d": false,
"multiple": false,
"double-border": false,
"tooltip": "beans & rice",
"link": "",
"icon": null,
"iconPosition": "",
"blend": false,
"fields": null,
"methods": null,
"columns": null,
"label": "&∈",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "N1",
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 21,
"labelHeight": 21,
"labelPosition": "INSIDE_MIDDLE_CENTER",
"zIndex": 0,
"level": 1
},
{
"id": "foo",
"type": "rectangle",
"pos": {
"x": 130,
"y": 12
},
"width": 69,
"height": 66,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"borderRadius": 0,
"fill": "B6",
"stroke": "B1",
"shadow": false,
"3d": false,
"multiple": false,
"double-border": false,
"tooltip": "",
"link": "",
"icon": null,
"iconPosition": "",
"blend": false,
"fields": null,
"methods": null,
"columns": null,
"label": "foo",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "N1",
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 24,
"labelHeight": 21,
"labelPosition": "INSIDE_MIDDLE_CENTER",
"zIndex": 0,
"level": 1
},
{
"id": "\"&bar\"",
"type": "rectangle",
"pos": {
"x": 219,
"y": 12
},
"width": 81,
"height": 66,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"borderRadius": 0,
"fill": "B6",
"stroke": "B1",
"shadow": false,
"3d": false,
"multiple": false,
"double-border": false,
"tooltip": "",
"link": "",
"icon": null,
"iconPosition": "",
"blend": false,
"fields": null,
"methods": null,
"columns": null,
"label": "&bar",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "N1",
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 36,
"labelHeight": 21,
"labelPosition": "INSIDE_MIDDLE_CENTER",
"zIndex": 0,
"level": 1
}
],
"connections": []
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 330 KiB

View file

@ -37,8 +37,8 @@
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 239,
"labelHeight": 150,
"labelWidth": 234,
"labelHeight": 145,
"zIndex": 0,
"level": 1
},
@ -77,8 +77,8 @@
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 160,
"labelHeight": 118,
"labelWidth": 155,
"labelHeight": 113,
"zIndex": 0,
"level": 1
},
@ -117,8 +117,8 @@
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 160,
"labelHeight": 118,
"labelWidth": 155,
"labelHeight": 113,
"zIndex": 0,
"level": 1
}

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><svg id="d2-svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="883" height="354" viewBox="-202 -202 883 354"><style type="text/css"><![CDATA[.shape {
<?xml version="1.0" encoding="utf-8"?><svg id="d2-svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="883" height="354" viewBox="-102 -102 883 354"><style type="text/css"><![CDATA[.shape {
shape-rendering: geometricPrecision;
stroke-linejoin: round;
}
@ -54,8 +54,8 @@
</text><text class="text-mono" x="0" y="3.000000em" xml:space="preserve">
</text><text class="text-mono" x="0" y="4.000000em" xml:space="preserve">&#160;&#160;<tspan fill="#0086b3">print</tspan>&#160;<tspan fill="#dd1144"></tspan><tspan fill="#dd1144">&quot;</tspan><tspan fill="#dd1144">world</tspan><tspan fill="#dd1144">&quot;</tspan>
</text><text class="text-mono" x="0" y="5.000000em" xml:space="preserve">
</text></g></g></g><mask id="1385563382" maskUnits="userSpaceOnUse" x="-100" y="-100" width="883" height="354">
<rect x="-100" y="-100" width="883" height="354" fill="white"></rect>
</text></g></g></g><mask id="60125551" maskUnits="userSpaceOnUse" x="-102" y="-102" width="883" height="354">
<rect x="-102" y="-102" width="883" height="354" fill="white"></rect>
</mask>
.text-mono-bold {

Before

Width:  |  Height:  |  Size: 519 KiB

After

Width:  |  Height:  |  Size: 519 KiB

View file

@ -37,8 +37,8 @@
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 239,
"labelHeight": 150,
"labelWidth": 234,
"labelHeight": 145,
"zIndex": 0,
"level": 1
},
@ -77,8 +77,8 @@
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 160,
"labelHeight": 118,
"labelWidth": 155,
"labelHeight": 113,
"zIndex": 0,
"level": 1
},
@ -117,8 +117,8 @@
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 160,
"labelHeight": 118,
"labelWidth": 155,
"labelHeight": 113,
"zIndex": 0,
"level": 1
}

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><svg id="d2-svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="803" height="354" viewBox="-190 -190 803 354"><style type="text/css"><![CDATA[.shape {
<?xml version="1.0" encoding="utf-8"?><svg id="d2-svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="803" height="354" viewBox="-90 -90 803 354"><style type="text/css"><![CDATA[.shape {
shape-rendering: geometricPrecision;
stroke-linejoin: round;
}
@ -54,8 +54,8 @@
</text><text class="text-mono" x="0" y="3.000000em" xml:space="preserve">
</text><text class="text-mono" x="0" y="4.000000em" xml:space="preserve">&#160;&#160;<tspan fill="#0086b3">print</tspan>&#160;<tspan fill="#dd1144"></tspan><tspan fill="#dd1144">&quot;</tspan><tspan fill="#dd1144">world</tspan><tspan fill="#dd1144">&quot;</tspan>
</text><text class="text-mono" x="0" y="5.000000em" xml:space="preserve">
</text></g></g></g><mask id="2161101331" maskUnits="userSpaceOnUse" x="-100" y="-100" width="803" height="354">
<rect x="-100" y="-100" width="803" height="354" fill="white"></rect>
</text></g></g></g><mask id="2533851426" maskUnits="userSpaceOnUse" x="-90" y="-90" width="803" height="354">
<rect x="-90" y="-90" width="803" height="354" fill="white"></rect>
</mask>
.text-mono-bold {

Before

Width:  |  Height:  |  Size: 519 KiB

After

Width:  |  Height:  |  Size: 519 KiB

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 806 KiB

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