merge updates
1
.gitignore
vendored
|
|
@ -5,3 +5,4 @@
|
|||
*.got.svg
|
||||
e2e_report.html
|
||||
bin
|
||||
out
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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 "$@"
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 "$@"
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
56
ci/release/changelogs/v0.2.0.md
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
Here's what a D2 diagram looks like in 0.1 (left) vs 0.2 (right):
|
||||
|
||||

|
||||
|
||||
|
||||
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.
|
||||
|
|
@ -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
|
|
@ -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 "$@"
|
||||
|
|
@ -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
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
325
d2graph/serde.go
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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
|
|
@ -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
69
d2ir/d2ir_test.go
Normal 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
|
|
@ -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
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:]...)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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{},
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
47
d2lib/d2.go
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 302 KiB After Width: | Height: | Size: 386 KiB |
|
Before Width: | Height: | Size: 284 KiB After Width: | Height: | Size: 305 KiB |
|
Before Width: | Height: | Size: 334 KiB After Width: | Height: | Size: 405 KiB |
|
Before Width: | Height: | Size: 228 KiB After Width: | Height: | Size: 241 KiB |
|
Before Width: | Height: | Size: 280 KiB After Width: | Height: | Size: 296 KiB |
|
Before Width: | Height: | Size: 227 KiB After Width: | Height: | Size: 241 KiB |
|
Before Width: | Height: | Size: 277 KiB After Width: | Height: | Size: 291 KiB |
64
d2renderers/d2sketch/testdata/crows_feet/sketch.exp.svg
vendored
Normal file
|
After Width: | Height: | Size: 301 KiB |
|
Before Width: | Height: | Size: 340 KiB After Width: | Height: | Size: 357 KiB |
|
Before Width: | Height: | Size: 115 KiB After Width: | Height: | Size: 148 KiB |
|
Before Width: | Height: | Size: 418 KiB After Width: | Height: | Size: 513 KiB |
|
Before Width: | Height: | Size: 807 KiB After Width: | Height: | Size: 807 KiB |
|
Before Width: | Height: | Size: 655 KiB After Width: | Height: | Size: 655 KiB |
|
Before Width: | Height: | Size: 654 KiB After Width: | Height: | Size: 654 KiB |
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 197 KiB After Width: | Height: | Size: 196 KiB |
|
Before Width: | Height: | Size: 240 KiB After Width: | Height: | Size: 240 KiB |
|
Before Width: | Height: | Size: 253 KiB After Width: | Height: | Size: 253 KiB |
|
Before Width: | Height: | Size: 188 KiB After Width: | Height: | Size: 188 KiB |
|
Before Width: | Height: | Size: 238 KiB After Width: | Height: | Size: 238 KiB |
|
Before Width: | Height: | Size: 188 KiB After Width: | Height: | Size: 188 KiB |
|
Before Width: | Height: | Size: 238 KiB After Width: | Height: | Size: 238 KiB |
|
Before Width: | Height: | Size: 299 KiB After Width: | Height: | Size: 299 KiB |
|
Before Width: | Height: | Size: 188 KiB After Width: | Height: | Size: 188 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 320 KiB After Width: | Height: | Size: 320 KiB |
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
2
e2etests/testdata/measured/empty-class/dagre/board.exp.json
generated
vendored
|
|
@ -10,7 +10,7 @@
|
|||
"y": 0
|
||||
},
|
||||
"width": 112,
|
||||
"height": 12,
|
||||
"height": 44,
|
||||
"opacity": 1,
|
||||
"strokeDash": 0,
|
||||
"strokeWidth": 2,
|
||||
|
|
|
|||
|
|
@ -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 |
2
e2etests/testdata/measured/empty-shape/dagre/board.exp.json
generated
vendored
|
|
@ -4,7 +4,7 @@
|
|||
"shapes": [
|
||||
{
|
||||
"id": "a",
|
||||
"type": "",
|
||||
"type": "rectangle",
|
||||
"pos": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
|
|
|
|||
|
|
@ -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 |
|
|
@ -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 |
130
e2etests/testdata/regression/ampersand-escape/dagre/board.exp.json
generated
vendored
Normal 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": []
|
||||
}
|
||||
59
e2etests/testdata/regression/ampersand-escape/dagre/sketch.exp.svg
vendored
Normal file
|
After Width: | Height: | Size: 330 KiB |
130
e2etests/testdata/regression/ampersand-escape/elk/board.exp.json
generated
vendored
Normal 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": []
|
||||
}
|
||||
59
e2etests/testdata/regression/ampersand-escape/elk/sketch.exp.svg
vendored
Normal file
|
After Width: | Height: | Size: 330 KiB |
12
e2etests/testdata/regression/code_leading_trailing_newlines/dagre/board.exp.json
generated
vendored
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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">  <tspan fill="#0086b3">print</tspan> <tspan fill="#dd1144"></tspan><tspan fill="#dd1144">"</tspan><tspan fill="#dd1144">world</tspan><tspan fill="#dd1144">"</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 |
12
e2etests/testdata/regression/code_leading_trailing_newlines/elk/board.exp.json
generated
vendored
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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">  <tspan fill="#0086b3">print</tspan> <tspan fill="#dd1144"></tspan><tspan fill="#dd1144">"</tspan><tspan fill="#dd1144">world</tspan><tspan fill="#dd1144">"</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 |
1722
e2etests/testdata/regression/dagre-disconnect/dagre/board.exp.json
generated
vendored
Normal file
57
e2etests/testdata/regression/dagre-disconnect/dagre/sketch.exp.svg
vendored
Normal file
|
After Width: | Height: | Size: 806 KiB |