Merge branch 'master' into link-layers
1
.gitignore
vendored
|
|
@ -6,3 +6,4 @@
|
|||
e2e_report.html
|
||||
bin
|
||||
out
|
||||
d2
|
||||
|
|
|
|||
10
Makefile
|
|
@ -7,17 +7,17 @@ all: fmt gen lint build test
|
|||
fmt:
|
||||
prefix "$@" ./ci/sub/bin/fmt.sh
|
||||
.PHONY: gen
|
||||
gen:
|
||||
gen: fmt
|
||||
prefix "$@" ./ci/gen.sh
|
||||
.PHONY: lint
|
||||
lint:
|
||||
lint: fmt
|
||||
prefix "$@" go vet --composites=false ./...
|
||||
.PHONY: build
|
||||
build:
|
||||
build: fmt
|
||||
prefix "$@" go build ./...
|
||||
.PHONY: test
|
||||
test:
|
||||
test: fmt
|
||||
prefix "$@" ./ci/test.sh
|
||||
.PHONY: race
|
||||
race:
|
||||
race: fmt
|
||||
prefix "$@" ./ci/test.sh --race ./...
|
||||
|
|
|
|||
40
README.md
|
|
@ -4,7 +4,6 @@
|
|||
A modern diagram scripting language that turns text to diagrams.
|
||||
</h2>
|
||||
|
||||
|
||||
[Docs](https://d2lang.com) | [Cheat sheet](./docs/assets/cheat_sheet.pdf) | [Comparisons](https://text-to-diagram.com) | [Playground](https://play.d2lang.com)
|
||||
|
||||
[](https://github.com/terrastruct/d2/actions/workflows/ci.yml)
|
||||
|
|
@ -25,24 +24,24 @@ https://user-images.githubusercontent.com/3120367/206125010-bd1fea8e-248a-43e7-8
|
|||
# Table of Contents
|
||||
|
||||
<!-- toc -->
|
||||
- <a href="#what-does-d2-look-like" id="toc-what-does-d2-look-like">What does D2 look like?</a>
|
||||
- <a href="#quickstart" id="toc-quickstart">Quickstart</a>
|
||||
- <a href="#install" id="toc-install">Install</a>
|
||||
- <a href="#d2-as-a-library" id="toc-d2-as-a-library">D2 as a library</a>
|
||||
- <a href="#themes" id="toc-themes">Themes</a>
|
||||
- <a href="#fonts" id="toc-fonts">Fonts</a>
|
||||
- <a href="#export-file-types" id="toc-export-file-types">Export file types</a>
|
||||
- <a href="#language-tooling" id="toc-language-tooling">Language tooling</a>
|
||||
- <a href="#plugins" id="toc-plugins">Plugins</a>
|
||||
- <a href="#comparison" id="toc-comparison">Comparison</a>
|
||||
- <a href="#contributing" id="toc-contributing">Contributing</a>
|
||||
- <a href="#license" id="toc-license">License</a>
|
||||
- <a href="#related" id="toc-related">Related</a>
|
||||
- <a href="#official-plugins" id="toc-official-plugins">Official plugins</a>
|
||||
- <a href="#community-plugins" id="toc-community-plugins">Community plugins</a>
|
||||
- <a href="#misc" id="toc-misc">Misc</a>
|
||||
- <a href="#faq" id="toc-faq">FAQ</a>
|
||||
- <a href="#open-source-projects-documenting-with-d2" id="toc-open-source-projects-documenting-with-d2">Open-source projects documenting with D2</a>
|
||||
- [What does D2 look like?](#what-does-d2-look-like)
|
||||
- [Quickstart](#quickstart)
|
||||
- [Install](#install)
|
||||
- [D2 as a library](#d2-as-a-library)
|
||||
- [Themes](#themes)
|
||||
- [Fonts](#fonts)
|
||||
- [Export file types](#export-file-types)
|
||||
- [Language tooling](#language-tooling)
|
||||
- [Plugins](#plugins)
|
||||
- [Comparison](#comparison)
|
||||
- [Contributing](#contributing)
|
||||
- [License](#license)
|
||||
- [Related](#related)
|
||||
- [Official plugins](#official-plugins)
|
||||
- [Community plugins](#community-plugins)
|
||||
- [Misc](#misc)
|
||||
- [FAQ](#faq)
|
||||
- [Open-source projects documenting with D2](#open-source-projects-documenting-with-d2)
|
||||
|
||||
## What does D2 look like?
|
||||
|
||||
|
|
@ -148,7 +147,7 @@ one, please see [./d2renderers/d2fonts](./d2renderers/d2fonts).
|
|||
|
||||
## Export file types
|
||||
|
||||
D2 currently supports SVG and PNG exports. More coming soon.
|
||||
D2 currently supports SVG, PNG and PDF exports. More coming soon.
|
||||
|
||||
## Language tooling
|
||||
|
||||
|
|
@ -226,6 +225,7 @@ let us know and we'll be happy to include it here!
|
|||
- **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
|
||||
|
||||
|
|
|
|||
|
|
@ -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,28 @@ 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 from the arm64 instance.
|
||||
# We could use QEMU with:
|
||||
# sudo -E apt-get install -y qemu qemu-user-static
|
||||
# But we don't as playwright dependencies do not install on QEMU on either arm64 or amd64.
|
||||
if [ "\$(uname -m)" = aarch64 ]; then
|
||||
if [ "\$(stat -c '%a' ~/.ssh/id_ed25519 2>/dev/null)" != 600 ]; then
|
||||
echo '$CI_TSTRUCT_ID_ED25519' >~/.ssh/id_ed25519
|
||||
chmod 600 ~/.ssh/id_ed25519
|
||||
fi
|
||||
if ! docker context ls | grep -qF ci-d2-linux-amd64; then
|
||||
docker context create ci-d2-linux-amd64 --docker "host=ssh://$CI_D2_LINUX_AMD64"
|
||||
fi
|
||||
if ! docker buildx ls | grep -qF 'd2 *'; then
|
||||
docker buildx create --use --name d2 --platform linux/arm64 default
|
||||
fi
|
||||
if ! docker buildx inspect d2 | grep -qF ci-d2-linux-amd64; then
|
||||
docker buildx create --append --name d2 --platform linux/amd64 ci-d2-linux-amd64
|
||||
fi
|
||||
fi
|
||||
|
||||
mkdir -p \$HOME/.local/bin
|
||||
mkdir -p \$HOME/.local/share/man
|
||||
EOF
|
||||
|
|
@ -387,7 +409,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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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_ARM64" .d2-build-lock
|
||||
sh_c gitsync "$CI_D2_LINUX_ARM64" src/d2
|
||||
sh_c rsync --archive --human-readable \
|
||||
"$BUILD_DIR/d2-$VERSION"-linux-*.tar.gz \
|
||||
"$CI_D2_LINUX_ARM64:src/d2/$BUILD_DIR/"
|
||||
sh_c ssh "$CI_D2_LINUX_ARM64" \
|
||||
"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,36 +1,5 @@
|
|||
#### 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)
|
||||
|
||||
- You can now use the reserved keywords `layers`/`scenarios`/`steps` to define diagrams
|
||||
with multiple levels of abstractions. [#714](https://github.com/terrastruct/d2/pull/714)
|
||||
Docs to come soon
|
||||
- [#416](https://github.com/terrastruct/d2/issues/416) was also fixed so 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 uses are
|
||||
`obj.shape` and `obj.style.double-border`.
|
||||
- Many other minor compiler bugs were fixed.
|
||||
|
||||
#### 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)
|
||||
- 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)
|
||||
- Improves package shape dimensions with short height. [#702](https://github.com/terrastruct/d2/pull/702)
|
||||
- Keeps person shape from becoming too distorted. [#702](https://github.com/terrastruct/d2/pull/702)
|
||||
- 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)
|
||||
|
||||
#### 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)
|
||||
- Images can now be set to sizes smaller than 128x128. [#702](https://github.com/terrastruct/d2/pull/702)
|
||||
- Fixes class height when there are no rows. [#756](https://github.com/terrastruct/d2/pull/756)
|
||||
|
|
|
|||
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.
|
||||
42
ci/release/changelogs/v0.2.1.md
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
Dark mode support has landed! Thanks to https://github.com/vfosnar for such a substaintial first-time contribution to D2. Only one dark theme option accompanies the support, so if you have a dark theme you like, please feel free to submit into D2!
|
||||
|
||||
https://user-images.githubusercontent.com/3120367/221057628-e474b040-4ecb-4177-bb81-a04c95a4648f.mp4
|
||||
|
||||
D2 is now usable in non-Latin languages (and emojis!), as the font-measuring accounts for multi-byte characters. Thanks https://github.com/bo-ku-ra for keeping this top of mind.
|
||||
|
||||
D2 0.2.0 vs 0.2.1:
|
||||
|
||||
<img width="1200" alt="japanese" src="https://user-images.githubusercontent.com/3120367/221058010-9a405cbf-a1dc-4005-8820-bf17d920105c.png">
|
||||
|
||||
Sketch mode's subtle hand-drawn texture adapts to background colors. Previously the streaks were too subtle on lighter backgrounds and too prominent on darker ones.
|
||||
|
||||
<img width="399" alt="sketch" src="https://user-images.githubusercontent.com/3120367/221042548-aee58a6c-e0c0-4e58-8d79-d0b609a9d750.png">
|
||||
|
||||
This release also fixes a number of non-trivial layout bugs made in v0.2.0, and has better error messages.
|
||||
|
||||
#### Features 🚀
|
||||
|
||||
- Dark theme support! See [docs](https://d2lang.com/tour/themes#dark-theme). [#613](https://github.com/terrastruct/d2/pull/613)
|
||||
- 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)
|
||||
- Dimensions can be set on containers (layout engine dependent). [#845](https://github.com/terrastruct/d2/pull/845)
|
||||
|
||||
#### Improvements 🧹
|
||||
|
||||
- Sketch mode's subtle hand-drawn texture adapts to background colors. [#613](https://github.com/terrastruct/d2/pull/613)
|
||||
- Improves label legibility for dagre containers by stopping container edges early if they would run into the label. [#880](https://github.com/terrastruct/d2/pull/880)
|
||||
- Cleaner watch mode logs without timestamps. [#830](https://github.com/terrastruct/d2/pull/830)
|
||||
- Remove duplicate success logs in watch mode. [#830](https://github.com/terrastruct/d2/pull/830)
|
||||
- CLI reports when a feature is incompatible with layout engine, instead of silently ignoring. [#845](https://github.com/terrastruct/d2/pull/845)
|
||||
- `near` key set to direct parent or ancestor throws an appropriate error message. [#851](https://github.com/terrastruct/d2/pull/851)
|
||||
- Dimensions and positions are able to be set from API. [#853](https://github.com/terrastruct/d2/pull/853)
|
||||
|
||||
#### Bugfixes ⛑️
|
||||
|
||||
- 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)
|
||||
- Bounding boxes weren't accounting for icons placed on the boundaries. [#879](https://github.com/terrastruct/d2/pull/879)
|
||||
- Sequence diagrams using special characters in object IDs could cause rendering bugs. [#856](https://github.com/terrastruct/d2/issues/856)
|
||||
- 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)
|
||||
- `steps` with non-map fields could cause panics. [#783](https://github.com/terrastruct/d2/pull/783)
|
||||
32
ci/release/changelogs/v0.2.2.md
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
`style` keywords now apply at the root level, letting you style the diagram background and frame like so:
|
||||
|
||||

|
||||
[playground link](https://play.d2lang.com/?script=tFRdaxs7EH3fXzEP15cErry-GwJFlIC9cdJAXIL9kJeCUbSTtchacqVZ56P4v5eRd9d2k9LkofjFGo1mzplzdmIVCT8SAJ6mhBGaEhOAbT8J58o_jKp6F9pCkPApATjQRgL5GpNNkpChtmgcrIQvplxAhWuswNh7rwL5WlPtY1memATCJ0oAeMwSyK2ERkvoY98OI0AnpITT0wRgww3VY5BQB4EqkPg_Zh4-itTIKxtWyqOlGN2btIRtrQQgr1xd3HtnqX3NZpGwIFoFmaZ8Cn1C37Loa7dM1WNIvyI9Ov9gbNnLBv_2skHuLKGlXjY4x8qs0T-nw6V6cVbEJhfcZF6xH8Vd2Q_rsoG1h_wNlA1OgNvhxS7tvSBnqGtv6Pm_Xja4KtBS83-Ld7mqjLIa0-HtTNwOL16j27yeE4gzmCj_gGRsCcEQ8v2fIvwqn05iqemkJdKYYYFPqnR2N4_-SdH4q2k_O_moPDNyXpXYSjA7EZeV0gb9ryTfoMfdAikyGlQISIFTsLqTML4ewdG1UwXcqYpH54__gm3GlQpktOBGYhQbGVuK8fVo3jwXFV-1GN5BKaIf3lyBx-81ho4S343zjE_jPOt0OTRluxQ7TbbGeD_p-aV39Yo9yCnpOM-EsYEYvNDOkjL2NyyGNS1ghn5tdHSVR15ovH4mSGqNPiBMmxgcffuHf8cNG-bW5u-FWN1LJNA18U7HOIpc6QV-yGMvtcd0zh-RsgZDOuQA68mVetlgioUJvWxw411Ra9aXC-2--631zVKVnctbsPBZnIHmOiySjqgPBrFJkni-8W5tCvQhLsX-fk73ZpfzMwAA__8%3D&)
|
||||
|
||||
(also showcases a little 3d hexagon, newly supported thanks to our newest contributor @JettChenT !)
|
||||
|
||||
PDF is also now supported as an export format:
|
||||
|
||||
[demo.pdf](https://github.com/terrastruct/d2/files/10846644/demo.pdf)
|
||||
|
||||
#### Features 🚀
|
||||
|
||||
- PDF exports. See [docs](https://d2lang.com/tour/exports#pdf). [#120](https://github.com/terrastruct/d2/issues/120)
|
||||
- Diagram background and frame can be added and styled. See [docs](https://d2lang.com/tour/style#root). [#910](https://github.com/terrastruct/d2/pull/910)
|
||||
- `3d` works on `hexagon` shapes. [#869](https://github.com/terrastruct/d2/issues/869)
|
||||
- The arm64 docker container supports rendering diagrams to PNGs. [#917](https://github.com/terrastruct/d2/pull/917)
|
||||
|
||||
#### Improvements 🧹
|
||||
|
||||
- `near` key set to sequence diagram children get an appropriate error message. [#899](https://github.com/terrastruct/d2/pull/899)
|
||||
- `class` and `sql_table` shape respect `font-color` styling as header font color. [#899](https://github.com/terrastruct/d2/pull/899)
|
||||
- SVG fits to screen by default in both watch mode and as a standalone SVG (this time with just CSS, no JS). [#725](https://github.com/terrastruct/d2/pull/725)
|
||||
- Only chromium is installed when rendering png diagrams instead of also installing webkit and firefox. [#835](https://github.com/terrastruct/d2/issues/835)
|
||||
- Multiboard output is now self-contained and less confusing. See [#923](https://github.com/terrastruct/d2/pull/923)
|
||||
|
||||
#### Bugfixes ⛑️
|
||||
|
||||
- Error reported when no actors are declared in sequence diagram. [#886](https://github.com/terrastruct/d2/pull/886)
|
||||
- Fixes img bundling on image shapes. [#889](https://github.com/terrastruct/d2/issues/889)
|
||||
- `class` shape as sequence diagram actors had wrong colors. [#899](https://github.com/terrastruct/d2/pull/899)
|
||||
- Fixes regression in last release where some hex codes were not working. [#922](https://github.com/terrastruct/d2/pull/922)
|
||||
|
|
@ -7,9 +7,8 @@ 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
|
||||
# https://github.com/microsoft/playwright/issues/18319
|
||||
# Hopefully soon.
|
||||
RUN if [ "$TARGETARCH" = amd64 ]; then npx playwright install-deps; fi
|
||||
# See https://github.com/microsoft/playwright/issues/18319
|
||||
RUN npx playwright@1.31.1 install-deps chromium
|
||||
|
||||
RUN adduser --gecos '' --disabled-password debian \
|
||||
&& echo "debian ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers.d/nopasswd
|
||||
|
|
@ -21,14 +20,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 if [ "$TARGETARCH" = amd64 ]; then d2 init-playwright; fi
|
||||
RUN d2 init-playwright
|
||||
|
||||
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 "$@"
|
||||
|
|
@ -66,6 +66,14 @@ Port listening address when used with
|
|||
.It Fl t , -theme Ar 0
|
||||
Set the diagram theme ID
|
||||
.Ns .
|
||||
.It Fl -dark-theme Ar -1
|
||||
The theme to use when the viewer's browser is in dark mode. When left unset
|
||||
.Fl -theme
|
||||
is used for both light and dark mode. Be aware that explicit styles set in D2 code will
|
||||
still be applied and this may produce unexpected results. We plan on resolving this by
|
||||
making style maps in D2 light/dark mode specific. See
|
||||
.Lk https://github.com/terrastruct/d2/issues/831
|
||||
.Ns .
|
||||
.It Fl s , -sketch Ar false
|
||||
Renders the diagram to look like it was sketched by hand
|
||||
.Ns .
|
||||
|
|
|
|||
2
ci/sub
|
|
@ -1 +1 @@
|
|||
Subproject commit 2009cdd523e00cc2e9b8ba804095f71ca70d5671
|
||||
Subproject commit 5198280010adc30aabb611579579916abfe20a45
|
||||
|
|
@ -593,8 +593,11 @@ type Key struct {
|
|||
Value ValueBox `json:"value"`
|
||||
}
|
||||
|
||||
// TODO there's more stuff to compare
|
||||
// TODO maybe need to compare Primary
|
||||
func (mk1 *Key) Equals(mk2 *Key) bool {
|
||||
if mk1.Ampersand != mk2.Ampersand {
|
||||
return false
|
||||
}
|
||||
if (mk1.Key == nil) != (mk2.Key == nil) {
|
||||
return false
|
||||
}
|
||||
|
|
@ -624,6 +627,16 @@ func (mk1 *Key) Equals(mk2 *Key) bool {
|
|||
}
|
||||
}
|
||||
}
|
||||
if mk1.EdgeKey != nil {
|
||||
if len(mk1.EdgeKey.Path) != len(mk2.EdgeKey.Path) {
|
||||
return false
|
||||
}
|
||||
for i, id := range mk1.EdgeKey.Path {
|
||||
if id.Unbox().ScalarString() != mk2.EdgeKey.Path[i].Unbox().ScalarString() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if mk1.Value.Map != nil {
|
||||
if len(mk1.Value.Map.Nodes) != len(mk2.Value.Map.Nodes) {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ 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())),
|
||||
|
|
@ -62,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
|
||||
}
|
||||
|
|
@ -95,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
|
||||
}
|
||||
|
|
@ -154,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
|
||||
}
|
||||
|
|
@ -191,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 {
|
||||
|
|
|
|||
|
|
@ -128,7 +128,7 @@ func test(t *testing.T, textPath, text string) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = d2exporter.Export(ctx, g, 0, nil)
|
||||
_, err = d2exporter.Export(ctx, g, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package main
|
||||
package d2cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package main
|
||||
package d2cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
562
d2cli/main.go
Normal file
|
|
@ -0,0 +1,562 @@
|
|||
package d2cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/playwright-community/playwright-go"
|
||||
"github.com/spf13/pflag"
|
||||
"go.uber.org/multierr"
|
||||
|
||||
"oss.terrastruct.com/util-go/go2"
|
||||
"oss.terrastruct.com/util-go/xmain"
|
||||
|
||||
"oss.terrastruct.com/d2/d2lib"
|
||||
"oss.terrastruct.com/d2/d2plugin"
|
||||
"oss.terrastruct.com/d2/d2renderers/d2fonts"
|
||||
"oss.terrastruct.com/d2/d2renderers/d2svg"
|
||||
"oss.terrastruct.com/d2/d2renderers/d2svg/appendix"
|
||||
"oss.terrastruct.com/d2/d2target"
|
||||
"oss.terrastruct.com/d2/d2themes"
|
||||
"oss.terrastruct.com/d2/d2themes/d2themescatalog"
|
||||
"oss.terrastruct.com/d2/lib/imgbundler"
|
||||
ctxlog "oss.terrastruct.com/d2/lib/log"
|
||||
pdflib "oss.terrastruct.com/d2/lib/pdf"
|
||||
"oss.terrastruct.com/d2/lib/png"
|
||||
"oss.terrastruct.com/d2/lib/textmeasure"
|
||||
"oss.terrastruct.com/d2/lib/version"
|
||||
|
||||
"cdr.dev/slog"
|
||||
"cdr.dev/slog/sloggers/sloghuman"
|
||||
)
|
||||
|
||||
func Run(ctx context.Context, ms *xmain.State) (err error) {
|
||||
// :(
|
||||
ctx = DiscardSlog(ctx)
|
||||
|
||||
// These should be kept up-to-date with the d2 man page
|
||||
watchFlag, err := ms.Opts.Bool("D2_WATCH", "watch", "w", false, "watch for changes to input and live reload. Use $HOST and $PORT to specify the listening address.\n(default localhost:0, which is will open on a randomly available local port).")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hostFlag := ms.Opts.String("HOST", "host", "h", "localhost", "host listening address when used with watch")
|
||||
portFlag := ms.Opts.String("PORT", "port", "p", "0", "port listening address when used with watch")
|
||||
bundleFlag, err := ms.Opts.Bool("D2_BUNDLE", "bundle", "b", true, "when outputting SVG, bundle all assets and layers into the output file")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
forceAppendixFlag, err := ms.Opts.Bool("D2_FORCE_APPENDIX", "force-appendix", "", false, "an appendix for tooltips and links is added to PNG exports since they are not interactive. --force-appendix adds an appendix to SVG exports as well")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
debugFlag, err := ms.Opts.Bool("DEBUG", "debug", "d", false, "print debug logs.")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
layoutFlag := ms.Opts.String("D2_LAYOUT", "layout", "l", "dagre", `the layout engine used`)
|
||||
themeFlag, err := ms.Opts.Int64("D2_THEME", "theme", "t", 0, "the diagram theme ID")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
darkThemeFlag, err := ms.Opts.Int64("D2_DARK_THEME", "dark-theme", "", -1, "The theme to use when the viewer's browser is in dark mode. When left unset -theme is used for both light and dark mode. Be aware that explicit styles set in D2 code will still be applied and this may produce unexpected results. We plan on resolving this by making style maps in D2 light/dark mode specific. See https://github.com/terrastruct/d2/issues/831.")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
padFlag, err := ms.Opts.Int64("D2_PAD", "pad", "", d2svg.DEFAULT_PADDING, "pixels padded around the rendered diagram")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
versionFlag, err := ms.Opts.Bool("", "version", "v", false, "get the version")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sketchFlag, err := ms.Opts.Bool("D2_SKETCH", "sketch", "s", false, "render the diagram to look like it was sketched by hand")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ps, err := d2plugin.ListPlugins(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = populateLayoutOpts(ctx, ms, ps)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = ms.Opts.Flags.Parse(ms.Opts.Args)
|
||||
if !errors.Is(err, pflag.ErrHelp) && err != nil {
|
||||
return xmain.UsageErrorf("failed to parse flags: %v", err)
|
||||
}
|
||||
|
||||
if errors.Is(err, pflag.ErrHelp) {
|
||||
help(ms)
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(ms.Opts.Flags.Args()) > 0 {
|
||||
switch ms.Opts.Flags.Arg(0) {
|
||||
case "init-playwright":
|
||||
return initPlaywright()
|
||||
case "layout":
|
||||
return layoutCmd(ctx, ms, ps)
|
||||
case "themes":
|
||||
themesCmd(ctx, ms)
|
||||
return nil
|
||||
case "fmt":
|
||||
return fmtCmd(ctx, ms)
|
||||
case "version":
|
||||
if len(ms.Opts.Flags.Args()) > 1 {
|
||||
return xmain.UsageErrorf("version subcommand accepts no arguments")
|
||||
}
|
||||
fmt.Println(version.Version)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if *debugFlag {
|
||||
ms.Env.Setenv("DEBUG", "1")
|
||||
}
|
||||
|
||||
var inputPath string
|
||||
var outputPath string
|
||||
|
||||
if len(ms.Opts.Flags.Args()) == 0 {
|
||||
if versionFlag != nil && *versionFlag {
|
||||
fmt.Println(version.Version)
|
||||
return nil
|
||||
}
|
||||
help(ms)
|
||||
return nil
|
||||
} else if len(ms.Opts.Flags.Args()) >= 3 {
|
||||
return xmain.UsageErrorf("too many arguments passed")
|
||||
}
|
||||
|
||||
if len(ms.Opts.Flags.Args()) >= 1 {
|
||||
inputPath = ms.Opts.Flags.Arg(0)
|
||||
}
|
||||
if len(ms.Opts.Flags.Args()) >= 2 {
|
||||
outputPath = ms.Opts.Flags.Arg(1)
|
||||
} else {
|
||||
if inputPath == "-" {
|
||||
outputPath = "-"
|
||||
} else {
|
||||
outputPath = renameExt(inputPath, ".svg")
|
||||
}
|
||||
}
|
||||
|
||||
match := d2themescatalog.Find(*themeFlag)
|
||||
if match == (d2themes.Theme{}) {
|
||||
return xmain.UsageErrorf("-t[heme] could not be found. The available options are:\n%s\nYou provided: %d", d2themescatalog.CLIString(), *themeFlag)
|
||||
}
|
||||
ms.Log.Debug.Printf("using theme %s (ID: %d)", match.Name, *themeFlag)
|
||||
|
||||
if *darkThemeFlag == -1 {
|
||||
darkThemeFlag = nil // TODO this is a temporary solution: https://github.com/terrastruct/util-go/issues/7
|
||||
}
|
||||
if darkThemeFlag != nil {
|
||||
match = d2themescatalog.Find(*darkThemeFlag)
|
||||
if match == (d2themes.Theme{}) {
|
||||
return xmain.UsageErrorf("--dark-theme could not be found. The available options are:\n%s\nYou provided: %d", d2themescatalog.CLIString(), *darkThemeFlag)
|
||||
}
|
||||
ms.Log.Debug.Printf("using dark theme %s (ID: %d)", match.Name, *darkThemeFlag)
|
||||
}
|
||||
|
||||
plugin, err := d2plugin.FindPlugin(ctx, ps, *layoutFlag)
|
||||
if err != nil {
|
||||
if errors.Is(err, exec.ErrNotFound) {
|
||||
return layoutNotFound(ctx, ps, *layoutFlag)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
err = d2plugin.HydratePluginOpts(ctx, ms, plugin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pinfo, err := plugin.Info(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
plocation := pinfo.Type
|
||||
if pinfo.Type == "binary" {
|
||||
plocation = fmt.Sprintf("executable plugin at %s", humanPath(pinfo.Path))
|
||||
}
|
||||
ms.Log.Debug.Printf("using layout plugin %s (%s)", *layoutFlag, plocation)
|
||||
|
||||
var pw png.Playwright
|
||||
if filepath.Ext(outputPath) == ".png" || filepath.Ext(outputPath) == ".pdf" {
|
||||
if darkThemeFlag != nil {
|
||||
ms.Log.Warn.Printf("--dark-theme cannot be used while exporting to another format other than .svg")
|
||||
darkThemeFlag = nil
|
||||
}
|
||||
pw, err = png.InitPlaywright()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
cleanupErr := pw.Cleanup()
|
||||
if err == nil {
|
||||
err = cleanupErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
if *watchFlag {
|
||||
if inputPath == "-" {
|
||||
return xmain.UsageErrorf("-w[atch] cannot be combined with reading input from stdin")
|
||||
}
|
||||
w, err := newWatcher(ctx, ms, watcherOpts{
|
||||
layoutPlugin: plugin,
|
||||
sketch: *sketchFlag,
|
||||
themeID: *themeFlag,
|
||||
darkThemeID: darkThemeFlag,
|
||||
pad: *padFlag,
|
||||
host: *hostFlag,
|
||||
port: *portFlag,
|
||||
inputPath: inputPath,
|
||||
outputPath: outputPath,
|
||||
bundle: *bundleFlag,
|
||||
forceAppendix: *forceAppendixFlag,
|
||||
pw: pw,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return w.run()
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, time.Minute*2)
|
||||
defer cancel()
|
||||
|
||||
_, written, err := compile(ctx, ms, plugin, *sketchFlag, *padFlag, *themeFlag, darkThemeFlag, inputPath, outputPath, *bundleFlag, *forceAppendixFlag, pw.Page)
|
||||
if err != nil {
|
||||
if written {
|
||||
return fmt.Errorf("failed to fully compile (partial render written): %w", err)
|
||||
}
|
||||
return fmt.Errorf("failed to compile: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, sketch bool, pad, themeID int64, darkThemeID *int64, inputPath, outputPath string, bundle, forceAppendix bool, page playwright.Page) (_ []byte, written bool, _ error) {
|
||||
start := time.Now()
|
||||
input, err := ms.ReadPath(inputPath)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
ruler, err := textmeasure.NewRuler()
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
layout := plugin.Layout
|
||||
opts := &d2lib.CompileOptions{
|
||||
Layout: layout,
|
||||
Ruler: ruler,
|
||||
}
|
||||
if sketch {
|
||||
opts.FontFamily = go2.Pointer(d2fonts.HandDrawn)
|
||||
}
|
||||
diagram, g, err := d2lib.Compile(ctx, string(input), opts)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
pluginInfo, err := plugin.Info(ctx)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
err = d2plugin.FeatureSupportCheck(pluginInfo, g)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
var svg []byte
|
||||
if filepath.Ext(outputPath) == ".pdf" {
|
||||
svg, err = renderPDF(ctx, ms, plugin, sketch, pad, themeID, outputPath, page, ruler, diagram, nil, nil)
|
||||
} else {
|
||||
compileDur := time.Since(start)
|
||||
svg, err = render(ctx, ms, compileDur, plugin, sketch, pad, themeID, darkThemeID, inputPath, outputPath, bundle, forceAppendix, page, ruler, diagram)
|
||||
}
|
||||
if err != nil {
|
||||
return svg, false, err
|
||||
}
|
||||
|
||||
if filepath.Ext(outputPath) == ".pdf" {
|
||||
dur := time.Since(start)
|
||||
ms.Log.Success.Printf("successfully compiled %s to %s in %s", inputPath, outputPath, dur)
|
||||
}
|
||||
|
||||
return svg, true, nil
|
||||
}
|
||||
|
||||
func render(ctx context.Context, ms *xmain.State, compileDur time.Duration, plugin d2plugin.Plugin, sketch bool, pad int64, themeID int64, darkThemeID *int64, inputPath, outputPath string, bundle, forceAppendix bool, page playwright.Page, ruler *textmeasure.Ruler, diagram *d2target.Diagram) ([]byte, error) {
|
||||
if diagram.Name != "" {
|
||||
ext := filepath.Ext(outputPath)
|
||||
outputPath = strings.TrimSuffix(outputPath, ext)
|
||||
outputPath = filepath.Join(outputPath, diagram.Name)
|
||||
outputPath += ext
|
||||
}
|
||||
|
||||
boardOutputPath := outputPath
|
||||
if len(diagram.Layers) > 0 || len(diagram.Scenarios) > 0 || len(diagram.Steps) > 0 {
|
||||
// Boards with subboards must be self-contained folders.
|
||||
ext := filepath.Ext(boardOutputPath)
|
||||
boardOutputPath = strings.TrimSuffix(boardOutputPath, ext)
|
||||
os.RemoveAll(boardOutputPath)
|
||||
boardOutputPath = filepath.Join(boardOutputPath, "index")
|
||||
boardOutputPath += ext
|
||||
}
|
||||
|
||||
layersOutputPath := outputPath
|
||||
if len(diagram.Scenarios) > 0 || len(diagram.Steps) > 0 {
|
||||
ext := filepath.Ext(layersOutputPath)
|
||||
layersOutputPath = strings.TrimSuffix(layersOutputPath, ext)
|
||||
layersOutputPath = filepath.Join(layersOutputPath, "layers")
|
||||
layersOutputPath += ext
|
||||
}
|
||||
scenariosOutputPath := outputPath
|
||||
if len(diagram.Layers) > 0 || len(diagram.Steps) > 0 {
|
||||
ext := filepath.Ext(scenariosOutputPath)
|
||||
scenariosOutputPath = strings.TrimSuffix(scenariosOutputPath, ext)
|
||||
scenariosOutputPath = filepath.Join(scenariosOutputPath, "scenarios")
|
||||
scenariosOutputPath += ext
|
||||
}
|
||||
stepsOutputPath := outputPath
|
||||
if len(diagram.Layers) > 0 || len(diagram.Scenarios) > 0 {
|
||||
ext := filepath.Ext(stepsOutputPath)
|
||||
stepsOutputPath = strings.TrimSuffix(stepsOutputPath, ext)
|
||||
stepsOutputPath = filepath.Join(stepsOutputPath, "steps")
|
||||
stepsOutputPath += ext
|
||||
}
|
||||
|
||||
for _, dl := range diagram.Layers {
|
||||
_, err := render(ctx, ms, compileDur, plugin, sketch, pad, themeID, darkThemeID, inputPath, layersOutputPath, bundle, forceAppendix, page, ruler, dl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
for _, dl := range diagram.Scenarios {
|
||||
_, err := render(ctx, ms, compileDur, plugin, sketch, pad, themeID, darkThemeID, inputPath, scenariosOutputPath, bundle, forceAppendix, page, ruler, dl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
for _, dl := range diagram.Steps {
|
||||
_, err := render(ctx, ms, compileDur, plugin, sketch, pad, themeID, darkThemeID, inputPath, stepsOutputPath, bundle, forceAppendix, page, ruler, dl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if !diagram.IsFolderOnly {
|
||||
start := time.Now()
|
||||
svg, err := _render(ctx, ms, plugin, sketch, pad, themeID, darkThemeID, boardOutputPath, bundle, forceAppendix, page, ruler, diagram)
|
||||
if err != nil {
|
||||
return svg, err
|
||||
}
|
||||
dur := compileDur + time.Since(start)
|
||||
ms.Log.Success.Printf("successfully compiled %s to %s in %s", inputPath, boardOutputPath, dur)
|
||||
return svg, nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func _render(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, sketch bool, pad int64, themeID int64, darkThemeID *int64, outputPath string, bundle, forceAppendix bool, page playwright.Page, ruler *textmeasure.Ruler, diagram *d2target.Diagram) ([]byte, error) {
|
||||
toPNG := filepath.Ext(outputPath) == ".png"
|
||||
svg, err := d2svg.Render(diagram, &d2svg.RenderOpts{
|
||||
Pad: int(pad),
|
||||
Sketch: sketch,
|
||||
ThemeID: themeID,
|
||||
DarkThemeID: darkThemeID,
|
||||
SetDimensions: toPNG,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
svg, err = plugin.PostProcess(ctx, svg)
|
||||
if err != nil {
|
||||
return svg, err
|
||||
}
|
||||
|
||||
svg, bundleErr := imgbundler.BundleLocal(ctx, ms, svg)
|
||||
if bundle {
|
||||
var bundleErr2 error
|
||||
svg, bundleErr2 = imgbundler.BundleRemote(ctx, ms, svg)
|
||||
bundleErr = multierr.Combine(bundleErr, bundleErr2)
|
||||
}
|
||||
if forceAppendix && !toPNG {
|
||||
svg = appendix.Append(diagram, ruler, svg)
|
||||
}
|
||||
|
||||
out := svg
|
||||
if toPNG {
|
||||
svg := appendix.Append(diagram, ruler, svg)
|
||||
|
||||
if !bundle {
|
||||
var bundleErr2 error
|
||||
svg, bundleErr2 = imgbundler.BundleRemote(ctx, ms, svg)
|
||||
bundleErr = multierr.Combine(bundleErr, bundleErr2)
|
||||
}
|
||||
|
||||
out, err = png.ConvertSVG(ms, page, svg)
|
||||
if err != nil {
|
||||
return svg, err
|
||||
}
|
||||
} else {
|
||||
if len(out) > 0 && out[len(out)-1] != '\n' {
|
||||
out = append(out, '\n')
|
||||
}
|
||||
}
|
||||
|
||||
err = os.MkdirAll(filepath.Dir(outputPath), 0755)
|
||||
if err != nil {
|
||||
return svg, err
|
||||
}
|
||||
err = ms.WritePath(outputPath, out)
|
||||
if err != nil {
|
||||
return svg, err
|
||||
}
|
||||
if bundleErr != nil {
|
||||
return svg, bundleErr
|
||||
}
|
||||
return svg, nil
|
||||
}
|
||||
|
||||
func renderPDF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, sketch bool, pad, themeID int64, outputPath string, page playwright.Page, ruler *textmeasure.Ruler, diagram *d2target.Diagram, pdf *pdflib.GoFPDF, boardPath []string) (svg []byte, err error) {
|
||||
var isRoot bool
|
||||
if pdf == nil {
|
||||
pdf = pdflib.Init()
|
||||
isRoot = true
|
||||
}
|
||||
|
||||
var currBoardPath []string
|
||||
// Root board doesn't have a name, so we use the output filename
|
||||
if diagram.Name == "" {
|
||||
ext := filepath.Ext(outputPath)
|
||||
trimmedPath := strings.TrimSuffix(outputPath, ext)
|
||||
splitPath := strings.Split(trimmedPath, "/")
|
||||
rootName := splitPath[len(splitPath)-1]
|
||||
currBoardPath = append(boardPath, rootName)
|
||||
} else {
|
||||
currBoardPath = append(boardPath, diagram.Name)
|
||||
}
|
||||
|
||||
if !diagram.IsFolderOnly {
|
||||
rootFill := diagram.Root.Fill
|
||||
// gofpdf will print the png img with a slight filter
|
||||
// make the bg fill within the png transparent so that the pdf bg fill is the only bg color present
|
||||
diagram.Root.Fill = "transparent"
|
||||
|
||||
svg, err = d2svg.Render(diagram, &d2svg.RenderOpts{
|
||||
Pad: int(pad),
|
||||
Sketch: sketch,
|
||||
SetDimensions: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
svg, err = plugin.PostProcess(ctx, svg)
|
||||
if err != nil {
|
||||
return svg, err
|
||||
}
|
||||
|
||||
svg, bundleErr := imgbundler.BundleLocal(ctx, ms, svg)
|
||||
svg, bundleErr2 := imgbundler.BundleRemote(ctx, ms, svg)
|
||||
bundleErr = multierr.Combine(bundleErr, bundleErr2)
|
||||
if bundleErr != nil {
|
||||
return svg, bundleErr
|
||||
}
|
||||
svg = appendix.Append(diagram, ruler, svg)
|
||||
|
||||
pngImg, err := png.ConvertSVG(ms, page, svg)
|
||||
if err != nil {
|
||||
return svg, err
|
||||
}
|
||||
|
||||
err = pdf.AddPDFPage(pngImg, currBoardPath, themeID, rootFill)
|
||||
if err != nil {
|
||||
return svg, err
|
||||
}
|
||||
}
|
||||
|
||||
for _, dl := range diagram.Layers {
|
||||
_, err := renderPDF(ctx, ms, plugin, sketch, pad, themeID, "", page, ruler, dl, pdf, currBoardPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
for _, dl := range diagram.Scenarios {
|
||||
_, err := renderPDF(ctx, ms, plugin, sketch, pad, themeID, "", page, ruler, dl, pdf, currBoardPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
for _, dl := range diagram.Steps {
|
||||
_, err := renderPDF(ctx, ms, plugin, sketch, pad, themeID, "", page, ruler, dl, pdf, currBoardPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if isRoot {
|
||||
err := pdf.Export(outputPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return svg, nil
|
||||
}
|
||||
|
||||
// newExt must include leading .
|
||||
func renameExt(fp string, newExt string) string {
|
||||
ext := filepath.Ext(fp)
|
||||
if ext == "" {
|
||||
return fp + newExt
|
||||
} else {
|
||||
return strings.TrimSuffix(fp, ext) + newExt
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: remove after removing slog
|
||||
func DiscardSlog(ctx context.Context) context.Context {
|
||||
return ctxlog.With(ctx, slog.Make(sloghuman.Sink(io.Discard)))
|
||||
}
|
||||
|
||||
func populateLayoutOpts(ctx context.Context, ms *xmain.State, ps []d2plugin.Plugin) error {
|
||||
pluginFlags, err := d2plugin.ListPluginFlags(ctx, ps)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, f := range pluginFlags {
|
||||
f.AddToOpts(ms.Opts)
|
||||
// Don't pollute the main d2 flagset with these. It'll be a lot
|
||||
ms.Opts.Flags.MarkHidden(f.Name)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func initPlaywright() error {
|
||||
pw, err := png.InitPlaywright()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return pw.Cleanup()
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package main_test
|
||||
package d2cli_test
|
||||
|
||||
import "testing"
|
||||
|
||||
|
|
@ -1,3 +1,13 @@
|
|||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#d2-svg-container > svg {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#d2-err {
|
||||
/* This style was copied from Chrome's svg parser error style. */
|
||||
white-space: pre-wrap;
|
||||
|
|
@ -11,8 +11,6 @@ function init(reconnectDelay) {
|
|||
const ws = new WebSocket(
|
||||
`ws://${window.location.host}${window.location.pathname}watch`
|
||||
);
|
||||
let isInit = true;
|
||||
let ratio;
|
||||
ws.onopen = () => {
|
||||
reconnectDelay = 1000;
|
||||
console.info("watch websocket opened");
|
||||
|
|
@ -33,27 +31,6 @@ function init(reconnectDelay) {
|
|||
// setting innerHTML to only the actual svg innards. However then you also need to parse
|
||||
// out the width, height and viewbox out of the top level SVG tag and update those manually.
|
||||
d2SVG.innerHTML = msg.svg;
|
||||
|
||||
const svgEl = d2SVG.querySelector("#d2-svg");
|
||||
let width = parseInt(svgEl.getAttribute("width"), 10);
|
||||
let height = parseInt(svgEl.getAttribute("height"), 10);
|
||||
if (isInit) {
|
||||
if (width > height) {
|
||||
if (width > window.innerWidth) {
|
||||
ratio = window.innerWidth / width;
|
||||
}
|
||||
} else if (height > window.innerHeight) {
|
||||
ratio = window.innerHeight / height;
|
||||
}
|
||||
// Scale svg fit to zoom
|
||||
isInit = false;
|
||||
}
|
||||
if (ratio) {
|
||||
// body padding is 8px
|
||||
svgEl.setAttribute("width", width * ratio - 16);
|
||||
svgEl.setAttribute("height", height * ratio - 16);
|
||||
}
|
||||
|
||||
d2ErrDiv.style.display = "none";
|
||||
}
|
||||
if (msg.err) {
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package main
|
||||
package d2cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
|
@ -41,6 +41,7 @@ var staticFS embed.FS
|
|||
type watcherOpts struct {
|
||||
layoutPlugin d2plugin.Plugin
|
||||
themeID int64
|
||||
darkThemeID *int64
|
||||
pad int64
|
||||
sketch bool
|
||||
host string
|
||||
|
|
@ -344,7 +345,7 @@ func (w *watcher) compileLoop(ctx context.Context) error {
|
|||
recompiledPrefix = "re"
|
||||
}
|
||||
|
||||
if filepath.Ext(w.outputPath) == ".png" && !w.pw.Browser.IsConnected() {
|
||||
if (filepath.Ext(w.outputPath) == ".png" || filepath.Ext(w.outputPath) == ".pdf") && !w.pw.Browser.IsConnected() {
|
||||
newPW, err := w.pw.RestartBrowser()
|
||||
if err != nil {
|
||||
broadcastErr := fmt.Errorf("issue encountered with PNG exporter: %w", err)
|
||||
|
|
@ -357,7 +358,7 @@ func (w *watcher) compileLoop(ctx context.Context) error {
|
|||
w.pw = newPW
|
||||
}
|
||||
|
||||
svg, _, err := compile(ctx, w.ms, w.layoutPlugin, w.sketch, w.pad, w.themeID, w.inputPath, w.outputPath, w.bundle, w.forceAppendix, w.pw.Page)
|
||||
svg, _, err := compile(ctx, w.ms, w.layoutPlugin, w.sketch, w.pad, w.themeID, w.darkThemeID, w.inputPath, w.outputPath, w.bundle, w.forceAppendix, w.pw.Page)
|
||||
errs := ""
|
||||
if err != nil {
|
||||
if len(svg) > 0 {
|
||||
|
|
@ -367,8 +368,6 @@ func (w *watcher) compileLoop(ctx context.Context) error {
|
|||
}
|
||||
errs = err.Error()
|
||||
w.ms.Log.Error.Print(errs)
|
||||
} else {
|
||||
w.ms.Log.Success.Printf("successfully %scompiled %v to %v", recompiledPrefix, w.inputPath, w.outputPath)
|
||||
}
|
||||
w.broadcast(&compileResult{
|
||||
SVG: string(svg),
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
//go:build dev
|
||||
// +build dev
|
||||
|
||||
package main
|
||||
package d2cli
|
||||
|
||||
func init() {
|
||||
devMode = true
|
||||
|
|
@ -72,8 +72,12 @@ func (c *compiler) compileBoard(g *d2graph.Graph, ir *d2ir.Map) *d2graph.Graph {
|
|||
c.compileBoardsField(g, ir, "scenarios")
|
||||
c.compileBoardsField(g, ir, "steps")
|
||||
|
||||
if d2ir.ParentMap(ir).CopyBase(nil).Equal(ir.CopyBase(nil)) {
|
||||
if len(g.Layers) > 0 || len(g.Scenarios) > 0 || len(g.Steps) > 0 {
|
||||
g.IsFolderOnly = true
|
||||
}
|
||||
}
|
||||
c.validateBoardLink(g, ir)
|
||||
|
||||
return g
|
||||
}
|
||||
|
||||
|
|
@ -163,6 +167,17 @@ func (c *compiler) compileField(obj *d2graph.Object, f *d2ir.Field) {
|
|||
return
|
||||
}
|
||||
|
||||
if obj.Parent != nil {
|
||||
if obj.Parent.Attributes.Shape.Value == d2target.ShapeSQLTable {
|
||||
c.errorf(f.LastRef().AST(), "sql_table columns cannot have children")
|
||||
return
|
||||
}
|
||||
if obj.Parent.Attributes.Shape.Value == d2target.ShapeClass {
|
||||
c.errorf(f.LastRef().AST(), "class fields cannot have children")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
obj = obj.EnsureChild(d2graphIDA([]string{f.Name}))
|
||||
if f.Primary() != nil {
|
||||
c.compileLabel(obj.Attributes, f)
|
||||
|
|
@ -181,7 +196,7 @@ func (c *compiler) compileField(obj *d2graph.Object, f *d2ir.Field) {
|
|||
}
|
||||
}
|
||||
scopeObjIDA := d2ir.IDA(fr.Context.ScopeMap)
|
||||
scopeObj, _ := obj.Graph.Root.HasChild(scopeObjIDA)
|
||||
scopeObj := obj.Graph.Root.EnsureChildIDVal(scopeObjIDA)
|
||||
obj.References = append(obj.References, d2graph.Reference{
|
||||
Key: fr.KeyPath,
|
||||
KeyPathIndex: fr.KeyPathIndex(),
|
||||
|
|
@ -258,7 +273,9 @@ func (c *compiler) compileReserved(attrs *d2graph.Attributes, f *d2ir.Field) {
|
|||
nearKey.Range = scalar.GetRange()
|
||||
attrs.NearKey = nearKey
|
||||
case "tooltip":
|
||||
attrs.Tooltip = scalar.ScalarString()
|
||||
attrs.Tooltip = &d2graph.Scalar{}
|
||||
attrs.Tooltip.Value = scalar.ScalarString()
|
||||
attrs.Tooltip.MapKey = f.LastPrimaryKey()
|
||||
case "width":
|
||||
_, err := strconv.Atoi(scalar.ScalarString())
|
||||
if err != nil {
|
||||
|
|
@ -277,10 +294,36 @@ func (c *compiler) compileReserved(attrs *d2graph.Attributes, f *d2ir.Field) {
|
|||
attrs.Height = &d2graph.Scalar{}
|
||||
attrs.Height.Value = scalar.ScalarString()
|
||||
attrs.Height.MapKey = f.LastPrimaryKey()
|
||||
case "top":
|
||||
v, err := strconv.Atoi(scalar.ScalarString())
|
||||
if err != nil {
|
||||
c.errorf(scalar, "non-integer top %#v: %s", scalar.ScalarString(), err)
|
||||
return
|
||||
}
|
||||
if v < 0 {
|
||||
c.errorf(scalar, "top must be a non-negative integer: %#v", scalar.ScalarString())
|
||||
return
|
||||
}
|
||||
attrs.Top = &d2graph.Scalar{}
|
||||
attrs.Top.Value = scalar.ScalarString()
|
||||
attrs.Top.MapKey = f.LastPrimaryKey()
|
||||
case "left":
|
||||
v, err := strconv.Atoi(scalar.ScalarString())
|
||||
if err != nil {
|
||||
c.errorf(scalar, "non-integer left %#v: %s", scalar.ScalarString(), err)
|
||||
return
|
||||
}
|
||||
if v < 0 {
|
||||
c.errorf(scalar, "left must be a non-negative integer: %#v", scalar.ScalarString())
|
||||
return
|
||||
}
|
||||
attrs.Left = &d2graph.Scalar{}
|
||||
attrs.Left.Value = scalar.ScalarString()
|
||||
attrs.Left.MapKey = f.LastPrimaryKey()
|
||||
case "link":
|
||||
attrs.Link = &d2graph.Scalar{}
|
||||
attrs.Link.Value = scalar.ScalarString()
|
||||
attrs.Link.MapKey = f.LastPrimaryKey()
|
||||
attrs.Link.MapKey.Range = scalar.GetRange()
|
||||
case "direction":
|
||||
dirs := []string{"up", "down", "right", "left"}
|
||||
if !go2.Contains(dirs, scalar.ScalarString()) {
|
||||
|
|
@ -358,6 +401,10 @@ func compileStyleFieldInit(attrs *d2graph.Attributes, f *d2ir.Field) {
|
|||
attrs.Width = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
||||
case "height":
|
||||
attrs.Height = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
||||
case "top":
|
||||
attrs.Top = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
||||
case "left":
|
||||
attrs.Left = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
||||
case "double-border":
|
||||
attrs.Style.DoubleBorder = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
||||
}
|
||||
|
|
@ -387,7 +434,7 @@ func (c *compiler) compileEdge(obj *d2graph.Object, e *d2ir.Edge) {
|
|||
edge.Attributes.Label.MapKey = e.LastPrimaryKey()
|
||||
for _, er := range e.References {
|
||||
scopeObjIDA := d2ir.IDA(er.Context.ScopeMap)
|
||||
scopeObj, _ := edge.Src.Graph.Root.HasChild(d2graphIDA(scopeObjIDA))
|
||||
scopeObj := edge.Src.Graph.Root.EnsureChildIDVal(scopeObjIDA)
|
||||
edge.References = append(edge.References, d2graph.EdgeReference{
|
||||
Edge: er.Context.Edge,
|
||||
MapKey: er.Context.Key,
|
||||
|
|
@ -564,14 +611,6 @@ func (c *compiler) validateKey(obj *d2graph.Object, f *d2ir.Field) {
|
|||
keyword := strings.ToLower(f.Name)
|
||||
_, isReserved := d2graph.ReservedKeywords[keyword]
|
||||
if isReserved {
|
||||
switch obj.Attributes.Shape.Value {
|
||||
case d2target.ShapeSQLTable, d2target.ShapeClass:
|
||||
default:
|
||||
if len(obj.Children) > 0 && (f.Name == "width" || f.Name == "height") {
|
||||
c.errorf(f.LastPrimaryKey(), fmt.Sprintf("%s cannot be used on container: %s", f.Name, obj.AbsID()))
|
||||
}
|
||||
}
|
||||
|
||||
switch obj.Attributes.Shape.Value {
|
||||
case d2target.ShapeCircle, d2target.ShapeSquare:
|
||||
checkEqual := (keyword == "width" && obj.Attributes.Height != nil) || (keyword == "height" && obj.Attributes.Width != nil)
|
||||
|
|
@ -583,8 +622,8 @@ func (c *compiler) validateKey(obj *d2graph.Object, f *d2ir.Field) {
|
|||
switch f.Name {
|
||||
case "style":
|
||||
if obj.Attributes.Style.ThreeDee != nil {
|
||||
if !strings.EqualFold(obj.Attributes.Shape.Value, d2target.ShapeSquare) && !strings.EqualFold(obj.Attributes.Shape.Value, d2target.ShapeRectangle) {
|
||||
c.errorf(obj.Attributes.Style.ThreeDee.MapKey, `key "3d" can only be applied to squares and rectangles`)
|
||||
if !strings.EqualFold(obj.Attributes.Shape.Value, d2target.ShapeSquare) && !strings.EqualFold(obj.Attributes.Shape.Value, d2target.ShapeRectangle) && !strings.EqualFold(obj.Attributes.Shape.Value, d2target.ShapeHexagon) {
|
||||
c.errorf(obj.Attributes.Style.ThreeDee.MapKey, `key "3d" can only be applied to squares, rectangles, and hexagons`)
|
||||
}
|
||||
}
|
||||
if obj.Attributes.Style.DoubleBorder != nil {
|
||||
|
|
@ -620,21 +659,37 @@ func (c *compiler) validateKey(obj *d2graph.Object, f *d2ir.Field) {
|
|||
func (c *compiler) validateNear(g *d2graph.Graph) {
|
||||
for _, obj := range g.Objects {
|
||||
if obj.Attributes.NearKey != nil {
|
||||
_, isKey := g.Root.HasChild(d2graph.Key(obj.Attributes.NearKey))
|
||||
nearObj, isKey := g.Root.HasChild(d2graph.Key(obj.Attributes.NearKey))
|
||||
_, isConst := d2graph.NearConstants[d2graph.Key(obj.Attributes.NearKey)[0]]
|
||||
if !isKey && !isConst {
|
||||
c.errorf(obj.Attributes.NearKey, "near key %#v must be the absolute path to a shape or one of the following constants: %s", d2format.Format(obj.Attributes.NearKey), strings.Join(d2graph.NearConstantsArray, ", "))
|
||||
continue
|
||||
}
|
||||
if !isKey && isConst && obj.Parent != g.Root {
|
||||
c.errorf(obj.Attributes.NearKey, "constant near keys can only be set on root level shapes")
|
||||
continue
|
||||
}
|
||||
if !isKey && isConst && len(obj.ChildrenArray) > 0 {
|
||||
c.errorf(obj.Attributes.NearKey, "constant near keys cannot be set on shapes with children")
|
||||
continue
|
||||
}
|
||||
if !isKey && isConst {
|
||||
if isKey {
|
||||
// Doesn't make sense to set near to an ancestor or descendant
|
||||
nearIsAncestor := false
|
||||
for curr := obj; curr != nil; curr = curr.Parent {
|
||||
if curr == nearObj {
|
||||
nearIsAncestor = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if nearIsAncestor {
|
||||
c.errorf(obj.Attributes.NearKey, "near keys cannot be set to an ancestor")
|
||||
continue
|
||||
}
|
||||
nearIsDescendant := false
|
||||
for curr := nearObj; curr != nil; curr = curr.Parent {
|
||||
if curr == obj {
|
||||
nearIsDescendant = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if nearIsDescendant {
|
||||
c.errorf(obj.Attributes.NearKey, "near keys cannot be set to an descendant")
|
||||
continue
|
||||
}
|
||||
if nearObj.OuterSequenceDiagram() != nil {
|
||||
c.errorf(obj.Attributes.NearKey, "near keys cannot be set to an object within sequence diagrams")
|
||||
continue
|
||||
}
|
||||
} else if isConst {
|
||||
is := false
|
||||
for _, e := range g.Edges {
|
||||
if e.Src == obj || e.Dst == obj {
|
||||
|
|
@ -646,6 +701,17 @@ func (c *compiler) validateNear(g *d2graph.Graph) {
|
|||
c.errorf(obj.Attributes.NearKey, "constant near keys cannot be set on connected shapes")
|
||||
continue
|
||||
}
|
||||
if obj.Parent != g.Root {
|
||||
c.errorf(obj.Attributes.NearKey, "constant near keys can only be set on root level shapes")
|
||||
continue
|
||||
}
|
||||
if len(obj.ChildrenArray) > 0 {
|
||||
c.errorf(obj.Attributes.NearKey, "constant near keys cannot be set on shapes with children")
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
c.errorf(obj.Attributes.NearKey, "near key %#v must be the absolute path to a shape or one of the following constants: %s", d2format.Format(obj.Attributes.NearKey), strings.Join(d2graph.NearConstantsArray, ", "))
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -86,7 +86,6 @@ x: {
|
|||
}
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: "dimensions_on_nonimage",
|
||||
|
||||
|
|
@ -114,6 +113,26 @@ 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: "positions_negative",
|
||||
text: `hey: {
|
||||
top: 200
|
||||
left: -200
|
||||
}
|
||||
`,
|
||||
expErr: `d2/testdata/d2compiler/TestCompile/positions_negative.d2:3:8: left must be a non-negative integer: "-200"`,
|
||||
},
|
||||
{
|
||||
name: "equal_dimensions_on_circle",
|
||||
|
||||
|
|
@ -153,8 +172,7 @@ d2/testdata/d2compiler/TestCompile/equal_dimensions_on_circle.d2:4:2: width and
|
|||
},
|
||||
},
|
||||
{
|
||||
name: "no_dimensions_on_containers",
|
||||
|
||||
name: "dimensions_on_containers",
|
||||
text: `
|
||||
containers: {
|
||||
circle container: {
|
||||
|
|
@ -201,13 +219,6 @@ containers: {
|
|||
}
|
||||
}
|
||||
`,
|
||||
expErr: `d2/testdata/d2compiler/TestCompile/no_dimensions_on_containers.d2:5:3: width cannot be used on container: containers.circle container
|
||||
d2/testdata/d2compiler/TestCompile/no_dimensions_on_containers.d2:15:3: width cannot be used on container: containers.diamond container
|
||||
d2/testdata/d2compiler/TestCompile/no_dimensions_on_containers.d2:16:3: height cannot be used on container: containers.diamond container
|
||||
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`,
|
||||
},
|
||||
{
|
||||
name: "dimension_with_style",
|
||||
|
|
@ -337,6 +348,17 @@ x: {
|
|||
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))
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "underscore_parent_not_root",
|
||||
|
||||
|
|
@ -1356,6 +1378,21 @@ x -> y: {
|
|||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nil_scope_obj_regression",
|
||||
|
||||
text: `a
|
||||
b: {
|
||||
_.a
|
||||
}
|
||||
`,
|
||||
assertions: func(t *testing.T, g *d2graph.Graph) {
|
||||
tassert.Equal(t, "a", g.Objects[0].ID)
|
||||
for _, ref := range g.Objects[0].References {
|
||||
tassert.NotNil(t, ref.ScopeObj)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "path_link",
|
||||
|
||||
|
|
@ -1377,6 +1414,29 @@ x -> y: {
|
|||
|
||||
text: `x.near: top-center
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "near-invalid",
|
||||
|
||||
text: `mongodb: MongoDB {
|
||||
perspective: perspective (View) {
|
||||
password
|
||||
}
|
||||
|
||||
explanation: |md
|
||||
perspective.model.js
|
||||
| {
|
||||
near: mongodb
|
||||
}
|
||||
}
|
||||
|
||||
a: {
|
||||
near: a.b
|
||||
b
|
||||
}
|
||||
`,
|
||||
expErr: `d2/testdata/d2compiler/TestCompile/near-invalid.d2:9:11: near keys cannot be set to an ancestor
|
||||
d2/testdata/d2compiler/TestCompile/near-invalid.d2:14:9: near keys cannot be set to an descendant`,
|
||||
},
|
||||
{
|
||||
name: "near_bad_constant",
|
||||
|
|
@ -1472,6 +1532,36 @@ d2/testdata/d2compiler/TestCompile/errors/reserved_icon_style.d2:2:9: 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",
|
||||
|
|
@ -1660,7 +1750,7 @@ x.y -> a.b: {
|
|||
|
||||
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, rectangles, and hexagons`,
|
||||
}, {
|
||||
name: "edge_column_index",
|
||||
text: `src: {
|
||||
|
|
@ -1699,6 +1789,44 @@ dst.id <-> src.dst_id
|
|||
assert.String(t, "sequence_diagram", g.Objects[0].Attributes.Shape.Value)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "near_sequence",
|
||||
|
||||
text: `x: {
|
||||
shape: sequence_diagram
|
||||
a
|
||||
}
|
||||
b.near: x.a
|
||||
`,
|
||||
expErr: `d2/testdata/d2compiler/TestCompile/near_sequence.d2:5:9: near keys cannot be set to an object within sequence diagrams`,
|
||||
},
|
||||
{
|
||||
name: "sequence-timestamp",
|
||||
|
||||
text: `shape: sequence_diagram
|
||||
a
|
||||
b
|
||||
|
||||
"04:20,11:20": {
|
||||
"loop through each table": {
|
||||
a."start_time = datetime.datetime.now"
|
||||
a -> b
|
||||
}
|
||||
}
|
||||
`,
|
||||
assertions: func(t *testing.T, g *d2graph.Graph) {
|
||||
tassert.Equal(t, 1, len(g.Edges))
|
||||
tassert.Equal(t, 5, len(g.Objects))
|
||||
tassert.Equal(t, "a", g.Objects[0].ID)
|
||||
tassert.Equal(t, "b", g.Objects[1].ID)
|
||||
tassert.Equal(t, `"04:20,11:20"`, g.Objects[2].ID)
|
||||
tassert.Equal(t, `loop through each table`, g.Objects[3].ID)
|
||||
tassert.Equal(t, 1, len(g.Objects[0].ChildrenArray))
|
||||
tassert.Equal(t, 0, len(g.Objects[1].ChildrenArray))
|
||||
tassert.Equal(t, 1, len(g.Objects[2].ChildrenArray))
|
||||
tassert.True(t, g.Edges[0].ContainedBy(g.Objects[3]))
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "root_sequence",
|
||||
|
||||
|
|
@ -2023,9 +2151,9 @@ layers: {
|
|||
}
|
||||
}
|
||||
`, "")
|
||||
assert.JSON(t, 2, len(g.Layers))
|
||||
assert.JSON(t, "one", g.Layers[0].Name)
|
||||
assert.JSON(t, "two", g.Layers[1].Name)
|
||||
assert.Equal(t, 2, len(g.Layers))
|
||||
assert.Equal(t, "one", g.Layers[0].Name)
|
||||
assert.Equal(t, "two", g.Layers[1].Name)
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -2056,6 +2184,37 @@ layers: {
|
|||
assert.Equal(t, 2, len(g.Layers[1].Steps))
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "isFolderOnly",
|
||||
run: func(t *testing.T) {
|
||||
g := assertCompile(t, `
|
||||
layers: {
|
||||
one: {
|
||||
santa
|
||||
}
|
||||
two: {
|
||||
clause
|
||||
scenarios: {
|
||||
seinfeld: {
|
||||
}
|
||||
missoula: {
|
||||
steps: {
|
||||
missus: one two three
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`, "")
|
||||
assert.True(t, g.IsFolderOnly)
|
||||
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].Scenarios))
|
||||
assert.False(t, g.Layers[1].Scenarios[0].IsFolderOnly)
|
||||
assert.False(t, g.Layers[1].Scenarios[1].IsFolderOnly)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "errs/duplicate_board",
|
||||
run: func(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -9,15 +9,14 @@ import (
|
|||
"oss.terrastruct.com/d2/d2graph"
|
||||
"oss.terrastruct.com/d2/d2renderers/d2fonts"
|
||||
"oss.terrastruct.com/d2/d2target"
|
||||
"oss.terrastruct.com/d2/d2themes"
|
||||
"oss.terrastruct.com/d2/d2themes/d2themescatalog"
|
||||
"oss.terrastruct.com/d2/lib/color"
|
||||
)
|
||||
|
||||
func Export(ctx context.Context, g *d2graph.Graph, themeID int64, fontFamily *d2fonts.FontFamily) (*d2target.Diagram, error) {
|
||||
theme := d2themescatalog.Find(themeID)
|
||||
|
||||
func Export(ctx context.Context, g *d2graph.Graph, fontFamily *d2fonts.FontFamily) (*d2target.Diagram, error) {
|
||||
diagram := d2target.NewDiagram()
|
||||
applyStyles(&diagram.Root, g.Root)
|
||||
diagram.Name = g.Name
|
||||
diagram.IsFolderOnly = g.IsFolderOnly
|
||||
if fontFamily == nil {
|
||||
fontFamily = go2.Pointer(d2fonts.SourceSansPro)
|
||||
}
|
||||
|
|
@ -25,27 +24,27 @@ func Export(ctx context.Context, g *d2graph.Graph, themeID int64, fontFamily *d2
|
|||
|
||||
diagram.Shapes = make([]d2target.Shape, len(g.Objects))
|
||||
for i := range g.Objects {
|
||||
diagram.Shapes[i] = toShape(g.Objects[i], &theme)
|
||||
diagram.Shapes[i] = toShape(g.Objects[i])
|
||||
}
|
||||
|
||||
diagram.Connections = make([]d2target.Connection, len(g.Edges))
|
||||
for i := range g.Edges {
|
||||
diagram.Connections[i] = toConnection(g.Edges[i], &theme)
|
||||
diagram.Connections[i] = toConnection(g.Edges[i])
|
||||
}
|
||||
|
||||
return diagram, nil
|
||||
}
|
||||
|
||||
func applyTheme(shape *d2target.Shape, obj *d2graph.Object, theme *d2themes.Theme) {
|
||||
shape.Stroke = obj.GetStroke(theme, shape.StrokeDash)
|
||||
shape.Fill = obj.GetFill(theme)
|
||||
func applyTheme(shape *d2target.Shape, obj *d2graph.Object) {
|
||||
shape.Stroke = obj.GetStroke(shape.StrokeDash)
|
||||
shape.Fill = obj.GetFill()
|
||||
if obj.Attributes.Shape.Value == d2target.ShapeText {
|
||||
shape.Color = theme.Colors.Neutrals.N1
|
||||
shape.Color = color.N1
|
||||
}
|
||||
if obj.Attributes.Shape.Value == d2target.ShapeSQLTable || obj.Attributes.Shape.Value == d2target.ShapeClass {
|
||||
shape.PrimaryAccentColor = theme.Colors.B2
|
||||
shape.SecondaryAccentColor = theme.Colors.AA2
|
||||
shape.NeutralAccentColor = theme.Colors.Neutrals.N2
|
||||
shape.PrimaryAccentColor = color.B2
|
||||
shape.SecondaryAccentColor = color.AA2
|
||||
shape.NeutralAccentColor = color.N2
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -100,7 +99,7 @@ func applyStyles(shape *d2target.Shape, obj *d2graph.Object) {
|
|||
}
|
||||
}
|
||||
|
||||
func toShape(obj *d2graph.Object, theme *d2themes.Theme) d2target.Shape {
|
||||
func toShape(obj *d2graph.Object) d2target.Shape {
|
||||
shape := d2target.BaseShape()
|
||||
shape.SetType(obj.Attributes.Shape.Value)
|
||||
shape.ID = obj.AbsID()
|
||||
|
|
@ -125,8 +124,8 @@ func toShape(obj *d2graph.Object, theme *d2themes.Theme) d2target.Shape {
|
|||
}
|
||||
|
||||
applyStyles(shape, obj)
|
||||
applyTheme(shape, obj, theme)
|
||||
shape.Color = text.GetColor(theme, shape.Italic)
|
||||
applyTheme(shape, obj)
|
||||
shape.Color = text.GetColor(shape.Italic)
|
||||
applyStyles(shape, obj)
|
||||
|
||||
switch obj.Attributes.Shape.Value {
|
||||
|
|
@ -136,10 +135,10 @@ func toShape(obj *d2graph.Object, theme *d2themes.Theme) 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
|
||||
|
|
@ -147,10 +146,17 @@ func toShape(obj *d2graph.Object, theme *d2themes.Theme) d2target.Shape {
|
|||
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
|
||||
shape.Link = obj.Attributes.Link.Value
|
||||
if obj.Attributes.Tooltip != nil {
|
||||
shape.Tooltip = obj.Attributes.Tooltip.Value
|
||||
}
|
||||
if obj.Attributes.Link != nil {
|
||||
shape.Link = obj.Attributes.Link.Value
|
||||
}
|
||||
shape.Icon = obj.Attributes.Icon
|
||||
if obj.IconPosition != nil {
|
||||
shape.IconPosition = *obj.IconPosition
|
||||
|
|
@ -159,11 +165,10 @@ func toShape(obj *d2graph.Object, theme *d2themes.Theme) d2target.Shape {
|
|||
return *shape
|
||||
}
|
||||
|
||||
func toConnection(edge *d2graph.Edge, theme *d2themes.Theme) d2target.Connection {
|
||||
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 {
|
||||
|
|
@ -208,7 +213,7 @@ func toConnection(edge *d2graph.Edge, theme *d2themes.Theme) d2target.Connection
|
|||
if edge.Attributes.Style.StrokeDash != nil {
|
||||
connection.StrokeDash, _ = strconv.ParseFloat(edge.Attributes.Style.StrokeDash.Value, 64)
|
||||
}
|
||||
connection.Stroke = edge.GetStroke(theme, connection.StrokeDash)
|
||||
connection.Stroke = edge.GetStroke(connection.StrokeDash)
|
||||
if edge.Attributes.Style.Stroke != nil {
|
||||
connection.Stroke = edge.Attributes.Style.Stroke.Value
|
||||
}
|
||||
|
|
@ -230,14 +235,16 @@ func toConnection(edge *d2graph.Edge, theme *d2themes.Theme) d2target.Connection
|
|||
connection.Animated, _ = strconv.ParseBool(edge.Attributes.Style.Animated.Value)
|
||||
}
|
||||
|
||||
connection.Tooltip = edge.Attributes.Tooltip
|
||||
if edge.Attributes.Tooltip != nil {
|
||||
connection.Tooltip = edge.Attributes.Tooltip.Value
|
||||
}
|
||||
connection.Icon = edge.Attributes.Icon
|
||||
|
||||
if edge.Attributes.Style.Italic != nil {
|
||||
connection.Italic, _ = strconv.ParseBool(edge.Attributes.Style.Italic.Value)
|
||||
}
|
||||
|
||||
connection.Color = text.GetColor(theme, connection.Italic)
|
||||
connection.Color = text.GetColor(connection.Italic)
|
||||
if edge.Attributes.Style.FontColor != nil {
|
||||
connection.Color = edge.Attributes.Style.FontColor.Value
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,16 +18,14 @@ import (
|
|||
"oss.terrastruct.com/d2/d2layouts/d2dagrelayout"
|
||||
"oss.terrastruct.com/d2/d2layouts/d2sequence"
|
||||
"oss.terrastruct.com/d2/d2target"
|
||||
"oss.terrastruct.com/d2/d2themes/d2themescatalog"
|
||||
"oss.terrastruct.com/d2/lib/geo"
|
||||
"oss.terrastruct.com/d2/lib/log"
|
||||
"oss.terrastruct.com/d2/lib/textmeasure"
|
||||
)
|
||||
|
||||
type testCase struct {
|
||||
name string
|
||||
dsl string
|
||||
themeID int64
|
||||
name string
|
||||
dsl string
|
||||
|
||||
assertions func(t *testing.T, d *d2target.Diagram)
|
||||
}
|
||||
|
|
@ -132,8 +130,7 @@ func testConnection(t *testing.T) {
|
|||
{
|
||||
// This is a regression test where a connection with stroke-dash of 0 on terrastruct flagship theme would have a diff color
|
||||
// than a connection without stroke dash
|
||||
themeID: d2themescatalog.FlagshipTerrastruct.ID,
|
||||
name: "theme_stroke-dash",
|
||||
name: "theme_stroke-dash",
|
||||
dsl: `x -> y: { style.stroke-dash: 0 }
|
||||
x -> y
|
||||
`,
|
||||
|
|
@ -168,16 +165,14 @@ func testLabel(t *testing.T) {
|
|||
func testTheme(t *testing.T) {
|
||||
tcs := []testCase{
|
||||
{
|
||||
name: "shape_without_bold",
|
||||
themeID: d2themescatalog.FlagshipTerrastruct.ID,
|
||||
name: "shape_without_bold",
|
||||
dsl: `x: {
|
||||
style.bold: false
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "shape_with_italic",
|
||||
themeID: d2themescatalog.FlagshipTerrastruct.ID,
|
||||
name: "shape_with_italic",
|
||||
dsl: `x: {
|
||||
style.italic: true
|
||||
}
|
||||
|
|
@ -187,7 +182,6 @@ func testTheme(t *testing.T) {
|
|||
name: "connection_without_italic",
|
||||
dsl: `x -> y: asdf { style.italic: false }
|
||||
`,
|
||||
themeID: d2themescatalog.FlagshipTerrastruct.ID,
|
||||
},
|
||||
{
|
||||
name: "connection_with_italic",
|
||||
|
|
@ -195,7 +189,6 @@ func testTheme(t *testing.T) {
|
|||
style.italic: true
|
||||
}
|
||||
`,
|
||||
themeID: d2themescatalog.FlagshipTerrastruct.ID,
|
||||
},
|
||||
{
|
||||
name: "connection_with_bold",
|
||||
|
|
@ -203,7 +196,6 @@ func testTheme(t *testing.T) {
|
|||
style.bold: true
|
||||
}
|
||||
`,
|
||||
themeID: d2themescatalog.FlagshipTerrastruct.ID,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -244,7 +236,7 @@ func run(t *testing.T, tc testCase) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
|
||||
got, err := d2exporter.Export(ctx, g, tc.themeID, nil)
|
||||
got, err := d2exporter.Export(ctx, g, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import (
|
|||
"oss.terrastruct.com/d2/d2renderers/d2fonts"
|
||||
"oss.terrastruct.com/d2/d2renderers/d2latex"
|
||||
"oss.terrastruct.com/d2/d2target"
|
||||
"oss.terrastruct.com/d2/d2themes"
|
||||
"oss.terrastruct.com/d2/lib/color"
|
||||
"oss.terrastruct.com/d2/lib/geo"
|
||||
"oss.terrastruct.com/d2/lib/shape"
|
||||
"oss.terrastruct.com/d2/lib/textmeasure"
|
||||
|
|
@ -28,8 +28,12 @@ const DEFAULT_SHAPE_SIZE = 100.
|
|||
const MIN_SHAPE_SIZE = 5
|
||||
|
||||
type Graph struct {
|
||||
Name string `json:"name"`
|
||||
AST *d2ast.Map `json:"ast"`
|
||||
Name string `json:"name"`
|
||||
// IsFolderOnly indicates a board or scenario itself makes no modifications from its
|
||||
// base. Folder only boards do not have a render and are used purely for organizing
|
||||
// the board tree.
|
||||
IsFolderOnly bool `json:"isFolderOnly"`
|
||||
AST *d2ast.Map `json:"ast"`
|
||||
|
||||
Root *Object `json:"root"`
|
||||
Edges []*Edge `json:"edges"`
|
||||
|
|
@ -95,13 +99,15 @@ type Attributes struct {
|
|||
Label Scalar `json:"label"`
|
||||
Style Style `json:"style"`
|
||||
Icon *url.URL `json:"icon,omitempty"`
|
||||
Tooltip string `json:"tooltip,omitempty"`
|
||||
Link Scalar `json:"link"`
|
||||
Tooltip *Scalar `json:"tooltip,omitempty"`
|
||||
Link *Scalar `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"`
|
||||
|
|
@ -334,14 +340,20 @@ func (l ContainerLevel) LabelSize() int {
|
|||
return d2fonts.FONT_SIZE_M
|
||||
}
|
||||
|
||||
func (obj *Object) GetFill(theme *d2themes.Theme) string {
|
||||
func (obj *Object) GetFill() string {
|
||||
level := int(obj.Level())
|
||||
shape := obj.Attributes.Shape.Value
|
||||
|
||||
if strings.EqualFold(shape, d2target.ShapeSQLTable) || strings.EqualFold(shape, d2target.ShapeClass) {
|
||||
return color.N1
|
||||
}
|
||||
|
||||
if obj.IsSequenceDiagramNote() {
|
||||
return theme.Colors.Neutrals.N7
|
||||
return color.N7
|
||||
} else if obj.IsSequenceDiagramGroup() {
|
||||
return theme.Colors.Neutrals.N5
|
||||
return color.N5
|
||||
} else if obj.Parent.IsSequenceDiagram() {
|
||||
return theme.Colors.B5
|
||||
return color.B5
|
||||
}
|
||||
|
||||
// fill for spans
|
||||
|
|
@ -349,85 +361,79 @@ func (obj *Object) GetFill(theme *d2themes.Theme) string {
|
|||
if sd != nil {
|
||||
level -= int(sd.Level())
|
||||
if level == 1 {
|
||||
return theme.Colors.B3
|
||||
return color.B3
|
||||
} else if level == 2 {
|
||||
return theme.Colors.B4
|
||||
return color.B4
|
||||
} else if level == 3 {
|
||||
return theme.Colors.B5
|
||||
return color.B5
|
||||
} else if level == 4 {
|
||||
return theme.Colors.Neutrals.N6
|
||||
return color.N6
|
||||
}
|
||||
return theme.Colors.Neutrals.N7
|
||||
return color.N7
|
||||
}
|
||||
|
||||
if obj.IsSequenceDiagram() {
|
||||
return theme.Colors.Neutrals.N7
|
||||
return color.N7
|
||||
}
|
||||
|
||||
shape := obj.Attributes.Shape.Value
|
||||
|
||||
if shape == "" || strings.EqualFold(shape, d2target.ShapeSquare) || strings.EqualFold(shape, d2target.ShapeCircle) || strings.EqualFold(shape, d2target.ShapeOval) || strings.EqualFold(shape, d2target.ShapeRectangle) {
|
||||
if level == 1 {
|
||||
if !obj.IsContainer() {
|
||||
return theme.Colors.B6
|
||||
return color.B6
|
||||
}
|
||||
return theme.Colors.B4
|
||||
return color.B4
|
||||
} else if level == 2 {
|
||||
return theme.Colors.B5
|
||||
return color.B5
|
||||
} else if level == 3 {
|
||||
return theme.Colors.B6
|
||||
return color.B6
|
||||
}
|
||||
return theme.Colors.Neutrals.N7
|
||||
return color.N7
|
||||
}
|
||||
|
||||
if strings.EqualFold(shape, d2target.ShapeCylinder) || strings.EqualFold(shape, d2target.ShapeStoredData) || strings.EqualFold(shape, d2target.ShapePackage) {
|
||||
if level == 1 {
|
||||
return theme.Colors.AA4
|
||||
return color.AA4
|
||||
}
|
||||
return theme.Colors.AA5
|
||||
return color.AA5
|
||||
}
|
||||
|
||||
if strings.EqualFold(shape, d2target.ShapeStep) || strings.EqualFold(shape, d2target.ShapePage) || strings.EqualFold(shape, d2target.ShapeDocument) {
|
||||
if level == 1 {
|
||||
return theme.Colors.AB4
|
||||
return color.AB4
|
||||
}
|
||||
return theme.Colors.AB5
|
||||
return color.AB5
|
||||
}
|
||||
|
||||
if strings.EqualFold(shape, d2target.ShapePerson) {
|
||||
return theme.Colors.B3
|
||||
return color.B3
|
||||
}
|
||||
if strings.EqualFold(shape, d2target.ShapeDiamond) {
|
||||
return theme.Colors.Neutrals.N4
|
||||
return color.N4
|
||||
}
|
||||
if strings.EqualFold(shape, d2target.ShapeCloud) || strings.EqualFold(shape, d2target.ShapeCallout) {
|
||||
return theme.Colors.Neutrals.N7
|
||||
return color.N7
|
||||
}
|
||||
if strings.EqualFold(shape, d2target.ShapeQueue) || strings.EqualFold(shape, d2target.ShapeParallelogram) || strings.EqualFold(shape, d2target.ShapeHexagon) {
|
||||
return theme.Colors.Neutrals.N5
|
||||
return color.N5
|
||||
}
|
||||
|
||||
if strings.EqualFold(shape, d2target.ShapeSQLTable) || strings.EqualFold(shape, d2target.ShapeClass) {
|
||||
return theme.Colors.Neutrals.N1
|
||||
}
|
||||
|
||||
return theme.Colors.Neutrals.N7
|
||||
return color.N7
|
||||
}
|
||||
|
||||
func (obj *Object) GetStroke(theme *d2themes.Theme, dashGapSize interface{}) string {
|
||||
func (obj *Object) GetStroke(dashGapSize interface{}) string {
|
||||
shape := obj.Attributes.Shape.Value
|
||||
if strings.EqualFold(shape, d2target.ShapeCode) ||
|
||||
strings.EqualFold(shape, d2target.ShapeText) {
|
||||
return theme.Colors.Neutrals.N1
|
||||
return color.N1
|
||||
}
|
||||
if strings.EqualFold(shape, d2target.ShapeClass) ||
|
||||
strings.EqualFold(shape, d2target.ShapeSQLTable) {
|
||||
return theme.Colors.Neutrals.N7
|
||||
return color.N7
|
||||
}
|
||||
if dashGapSize != 0.0 {
|
||||
return theme.Colors.B2
|
||||
return color.B2
|
||||
}
|
||||
return theme.Colors.B1
|
||||
return color.B1
|
||||
}
|
||||
|
||||
func (obj *Object) Level() ContainerLevel {
|
||||
|
|
@ -465,6 +471,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()
|
||||
|
|
@ -477,7 +488,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
|
||||
|
|
@ -553,6 +564,38 @@ func (obj *Object) HasChild(ids []string) (*Object, bool) {
|
|||
return child, true
|
||||
}
|
||||
|
||||
// Keep in sync with EnsureChild.
|
||||
func (obj *Object) EnsureChildIDVal(ids []string) *Object {
|
||||
if len(ids) == 0 {
|
||||
return obj
|
||||
}
|
||||
if len(ids) == 1 && ids[0] != "style" {
|
||||
_, ok := ReservedKeywords[ids[0]]
|
||||
if ok {
|
||||
return obj
|
||||
}
|
||||
}
|
||||
|
||||
id := ids[0]
|
||||
ids = ids[1:]
|
||||
|
||||
var child *Object
|
||||
for _, ch2 := range obj.ChildrenArray {
|
||||
if ch2.IDVal == id {
|
||||
child = ch2
|
||||
break
|
||||
}
|
||||
}
|
||||
if child == nil {
|
||||
child = obj.newObject(id)
|
||||
}
|
||||
|
||||
if len(ids) >= 1 {
|
||||
return child.EnsureChildIDVal(ids)
|
||||
}
|
||||
return child
|
||||
}
|
||||
|
||||
func (obj *Object) HasEdge(mk *d2ast.Key) (*Edge, bool) {
|
||||
ea, ok := obj.FindEdges(mk)
|
||||
if !ok {
|
||||
|
|
@ -795,17 +838,22 @@ 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())
|
||||
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())
|
||||
return nil, fmt.Errorf("dimensions for class method %#v not found", m.Text(fontSize))
|
||||
}
|
||||
maxWidth = go2.Max(maxWidth, mdims.Width)
|
||||
}
|
||||
|
|
@ -820,9 +868,9 @@ func (obj *Object) GetDefaultSize(mtexts []*d2target.MText, ruler *textmeasure.R
|
|||
// 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 {
|
||||
rowHeight := GetTextDimensions(mtexts, ruler, anyRowText, go2.Pointer(d2fonts.SourceCodePro)).Height + d2target.VerticalPadding
|
||||
|
|
@ -836,10 +884,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 {
|
||||
|
|
@ -915,11 +969,11 @@ type EdgeReference struct {
|
|||
ScopeObj *Object `json:"-"`
|
||||
}
|
||||
|
||||
func (e *Edge) GetStroke(theme *d2themes.Theme, dashGapSize interface{}) string {
|
||||
func (e *Edge) GetStroke(dashGapSize interface{}) string {
|
||||
if dashGapSize != 0.0 {
|
||||
return theme.Colors.B2
|
||||
return color.B2
|
||||
}
|
||||
return theme.Colors.B1
|
||||
return color.B1
|
||||
}
|
||||
|
||||
func (e *Edge) ArrowString() string {
|
||||
|
|
@ -1263,10 +1317,10 @@ func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler
|
|||
switch shapeType {
|
||||
case shape.TABLE_TYPE, shape.CLASS_TYPE, shape.CODE_TYPE, shape.IMAGE_TYPE:
|
||||
default:
|
||||
if obj.Attributes.Link.Value != "" {
|
||||
if obj.Attributes.Link != nil {
|
||||
paddingX += 32
|
||||
}
|
||||
if obj.Attributes.Tooltip != "" {
|
||||
if obj.Attributes.Tooltip != nil {
|
||||
paddingX += 32
|
||||
}
|
||||
}
|
||||
|
|
@ -1280,8 +1334,11 @@ func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler
|
|||
obj.Width = sideLength
|
||||
obj.Height = sideLength
|
||||
} else if desiredHeight == 0 || desiredWidth == 0 {
|
||||
if s.GetType() == shape.PERSON_TYPE {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1327,15 +1384,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)
|
||||
}
|
||||
}
|
||||
|
|
@ -1383,6 +1448,8 @@ var SimpleReservedKeywords = 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
|
||||
|
|
@ -1523,3 +1590,13 @@ func (g *Graph) SortEdgesByAST() {
|
|||
})
|
||||
g.Edges = edges
|
||||
}
|
||||
|
||||
func (obj *Object) IsDescendantOf(ancestor *Object) bool {
|
||||
if obj == ancestor {
|
||||
return true
|
||||
}
|
||||
if obj.Parent == nil {
|
||||
return false
|
||||
}
|
||||
return obj.Parent.IsDescendantOf(ancestor)
|
||||
}
|
||||
|
|
|
|||
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)
|
||||
|
|
|
|||
|
|
@ -38,7 +38,8 @@ func (c *compiler) compileScenarios(m *Map) {
|
|||
}
|
||||
|
||||
for _, sf := range scenarios.Fields {
|
||||
if sf.Map() == nil {
|
||||
if sf.Map() == nil || sf.Primary() != nil {
|
||||
c.errorf(sf.References[0].Context.Key, "invalid scenario")
|
||||
continue
|
||||
}
|
||||
base := m.CopyBase(sf)
|
||||
|
|
@ -59,8 +60,9 @@ func (c *compiler) compileSteps(m *Map) {
|
|||
return
|
||||
}
|
||||
for i, sf := range steps.Fields {
|
||||
if sf.Map() == nil {
|
||||
continue
|
||||
if sf.Map() == nil || sf.Primary() != nil {
|
||||
c.errorf(sf.References[0].Context.Key, "invalid step")
|
||||
break
|
||||
}
|
||||
var base *Map
|
||||
if i == 0 {
|
||||
|
|
|
|||
|
|
@ -420,6 +420,22 @@ steps: {
|
|||
assertQuery(t, m, 0, 0, nil, "steps.nuclear.quiche")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "steps_panic",
|
||||
run: func(t testing.TB) {
|
||||
_, err := compile(t, `steps: {
|
||||
shape: sql_table
|
||||
id: int {constraint: primary_key}
|
||||
}
|
||||
scenarios: {
|
||||
shape: sql_table
|
||||
hey: int {constraint: primary_key}
|
||||
}`)
|
||||
assert.ErrorString(t, err, `TestCompile/steps/steps_panic.d2:6:3: invalid scenario
|
||||
TestCompile/steps/steps_panic.d2:7:3: invalid scenario
|
||||
TestCompile/steps/steps_panic.d2:2:3: invalid step`)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "recursive",
|
||||
run: func(t testing.TB) {
|
||||
|
|
|
|||
109
d2ir/d2ir.go
|
|
@ -23,6 +23,7 @@ type Node interface {
|
|||
Parent() Node
|
||||
Primary() *Scalar
|
||||
Map() *Map
|
||||
Equal(n2 Node) bool
|
||||
|
||||
ast() d2ast.Node
|
||||
fmt.Stringer
|
||||
|
|
@ -139,7 +140,8 @@ func (s *Scalar) Copy(newParent Node) Node {
|
|||
return s
|
||||
}
|
||||
|
||||
func (s *Scalar) Equal(s2 *Scalar) bool {
|
||||
func (s *Scalar) Equal(n2 Node) bool {
|
||||
s2 := n2.(*Scalar)
|
||||
if _, ok := s.Value.(d2ast.String); ok {
|
||||
if _, ok = s2.Value.(d2ast.String); ok {
|
||||
return s.Value.ScalarString() == s2.Value.ScalarString()
|
||||
|
|
@ -187,6 +189,10 @@ func (m *Map) Copy(newParent Node) Node {
|
|||
|
||||
// CopyBase copies the map m without layers/scenarios/steps.
|
||||
func (m *Map) CopyBase(newParent Node) *Map {
|
||||
if m == nil {
|
||||
return (&Map{}).Copy(newParent).(*Map)
|
||||
}
|
||||
|
||||
layers := m.DeleteField("layers")
|
||||
scenarios := m.DeleteField("scenarios")
|
||||
steps := m.DeleteField("steps")
|
||||
|
|
@ -233,7 +239,7 @@ func NodeBoardKind(n Node) BoardKind {
|
|||
var f *Field
|
||||
switch n := n.(type) {
|
||||
case *Field:
|
||||
if n.Name == "" {
|
||||
if n.parent == nil {
|
||||
return BoardLayer
|
||||
}
|
||||
f = ParentField(n)
|
||||
|
|
@ -379,7 +385,9 @@ func (eid *EdgeID) Match(eid2 *EdgeID) bool {
|
|||
return true
|
||||
}
|
||||
|
||||
func (eid *EdgeID) resolveUnderscores(m *Map) (*EdgeID, *Map, error) {
|
||||
// resolve resolves both underscores and commons in eid.
|
||||
// It returns the new eid, containing map adjusted for underscores and common ida.
|
||||
func (eid *EdgeID) resolve(m *Map) (_ *EdgeID, _ *Map, common []string, _ error) {
|
||||
eid = eid.Copy()
|
||||
maxUnderscores := go2.Max(countUnderscores(eid.SrcPath), countUnderscores(eid.DstPath))
|
||||
for i := 0; i < maxUnderscores; i++ {
|
||||
|
|
@ -397,23 +405,20 @@ func (eid *EdgeID) resolveUnderscores(m *Map) (*EdgeID, *Map, error) {
|
|||
}
|
||||
m = ParentMap(m)
|
||||
if m == nil {
|
||||
return nil, nil, errors.New("invalid underscore")
|
||||
return nil, nil, nil, errors.New("invalid underscore")
|
||||
}
|
||||
}
|
||||
return eid, m, nil
|
||||
}
|
||||
|
||||
func (eid *EdgeID) trimCommon() (common []string, _ *EdgeID) {
|
||||
eid = eid.Copy()
|
||||
for len(eid.SrcPath) > 1 && len(eid.DstPath) > 1 {
|
||||
if !strings.EqualFold(eid.SrcPath[0], eid.DstPath[0]) {
|
||||
return common, eid
|
||||
return eid, m, common, nil
|
||||
}
|
||||
common = append(common, eid.SrcPath[0])
|
||||
eid.SrcPath = eid.SrcPath[1:]
|
||||
eid.DstPath = eid.DstPath[1:]
|
||||
}
|
||||
return common, eid
|
||||
|
||||
return eid, m, common, nil
|
||||
}
|
||||
|
||||
type Edge struct {
|
||||
|
|
@ -732,11 +737,10 @@ func (m *Map) DeleteField(ida ...string) *Field {
|
|||
}
|
||||
|
||||
func (m *Map) GetEdges(eid *EdgeID) []*Edge {
|
||||
eid, m, err := eid.resolveUnderscores(m)
|
||||
eid, m, common, err := eid.resolve(m)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
common, eid := eid.trimCommon()
|
||||
if len(common) > 0 {
|
||||
f := m.GetField(common...)
|
||||
if f == nil {
|
||||
|
|
@ -762,16 +766,12 @@ func (m *Map) CreateEdge(eid *EdgeID, refctx *RefContext) (*Edge, error) {
|
|||
return nil, d2parser.Errorf(refctx.Edge, "cannot create edge inside edge")
|
||||
}
|
||||
|
||||
eid, m, err := eid.resolveUnderscores(m)
|
||||
eid, m, common, err := eid.resolve(m)
|
||||
if err != nil {
|
||||
return nil, d2parser.Errorf(refctx.Edge, err.Error())
|
||||
}
|
||||
common, eid := eid.trimCommon()
|
||||
if len(common) > 0 {
|
||||
tmp := *refctx.Edge.Src
|
||||
kp := &tmp
|
||||
kp.Path = kp.Path[:len(common)]
|
||||
f, err := m.EnsureField(kp, nil)
|
||||
f, err := m.EnsureField(d2ast.MakeKeyPath(common), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -1033,11 +1033,12 @@ func parentPrimaryKey(n Node) *d2ast.Key {
|
|||
return nil
|
||||
}
|
||||
|
||||
// IDA returns the absolute path to n from the nearest board root.
|
||||
func IDA(n Node) (ida []string) {
|
||||
for {
|
||||
f, ok := n.(*Field)
|
||||
if ok {
|
||||
if f.Root() {
|
||||
if f.Root() || NodeBoardKind(f) != "" {
|
||||
reverseIDA(ida)
|
||||
return ida
|
||||
}
|
||||
|
|
@ -1059,3 +1060,73 @@ func reverseIDA(ida []string) {
|
|||
ida[len(ida)-i-1] = tmp
|
||||
}
|
||||
}
|
||||
|
||||
func (f *Field) Equal(n2 Node) bool {
|
||||
f2 := n2.(*Field)
|
||||
|
||||
if f.Name != f2.Name {
|
||||
return false
|
||||
}
|
||||
if !f.Primary_.Equal(f2.Primary_) {
|
||||
return false
|
||||
}
|
||||
if !f.Composite.Equal(f2.Composite) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (e *Edge) Equal(n2 Node) bool {
|
||||
e2 := n2.(*Edge)
|
||||
|
||||
if !e.ID.Match(e2.ID) {
|
||||
return false
|
||||
}
|
||||
if !e.Primary_.Equal(e2.Primary_) {
|
||||
return false
|
||||
}
|
||||
if !e.Map_.Equal(e2.Map_) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (a *Array) Equal(n2 Node) bool {
|
||||
a2 := n2.(*Array)
|
||||
|
||||
if len(a.Values) != len(a2.Values) {
|
||||
return false
|
||||
}
|
||||
|
||||
for i := range a.Values {
|
||||
if !a.Values[i].Equal(a2.Values[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (m *Map) Equal(n2 Node) bool {
|
||||
m2 := n2.(*Map)
|
||||
|
||||
if len(m.Fields) != len(m2.Fields) {
|
||||
return false
|
||||
}
|
||||
if len(m.Edges) != len(m2.Edges) {
|
||||
return false
|
||||
}
|
||||
|
||||
for i := range m.Fields {
|
||||
if !m.Fields[i].Equal(m2.Fields[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
for i := range m.Edges {
|
||||
if !m.Edges[i].Equal(m2.Edges[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,118 @@ 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 {
|
||||
isSrcDesc := e.Src.IsDescendantOf(obj)
|
||||
isDstDesc := e.Dst.IsDescendantOf(obj)
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
q := []*d2graph.Object{obj}
|
||||
// Downshift descendants and edges that have one endpoint connected to a descendant
|
||||
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 descendants move half, to maintain vertical padding
|
||||
if curr != obj {
|
||||
stepSize /= 2.
|
||||
}
|
||||
curr.TopLeft.Y += stepSize
|
||||
almostEqual := func(a, b float64) bool {
|
||||
return b-1 <= a && a <= b+1
|
||||
}
|
||||
shouldMove := func(p *geo.Point) bool {
|
||||
if curr != obj {
|
||||
return true
|
||||
}
|
||||
if isHorizontal {
|
||||
// Only move horizontal edges if they are connected to the top side of the shrinking container
|
||||
return almostEqual(p.Y, obj.TopLeft.Y-stepSize)
|
||||
} else {
|
||||
// Edge should only move if it's not connected to the bottom side of the shrinking container
|
||||
return !almostEqual(p.Y, obj.TopLeft.Y+obj.Height)
|
||||
}
|
||||
}
|
||||
for _, e := range g.Edges {
|
||||
if _, ok := movedEdges[e]; ok {
|
||||
continue
|
||||
}
|
||||
moveWholeEdge := false
|
||||
if e.Src == curr {
|
||||
// Don't move src points on side of container
|
||||
if almostEqual(e.Route[0].X, obj.TopLeft.X) || almostEqual(e.Route[0].X, obj.TopLeft.X+obj.Width) {
|
||||
// Unless the dst is also on a container
|
||||
if e.Dst.LabelHeight == nil || len(e.Dst.ChildrenArray) <= 0 {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if shouldMove(e.Route[0]) {
|
||||
if isHorizontal && e.Src.Parent != g.Root && e.Dst.Parent != g.Root {
|
||||
moveWholeEdge = true
|
||||
} else {
|
||||
e.Route[0].Y += stepSize
|
||||
}
|
||||
}
|
||||
}
|
||||
if !moveWholeEdge && e.Dst == curr {
|
||||
if shouldMove(e.Route[len(e.Route)-1]) {
|
||||
if isHorizontal && e.Dst.Parent != g.Root && e.Src.Parent != g.Root {
|
||||
moveWholeEdge = true
|
||||
} else {
|
||||
e.Route[len(e.Route)-1].Y += stepSize
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if moveWholeEdge {
|
||||
for _, p := range e.Route {
|
||||
p.Y += stepSize / 2.
|
||||
}
|
||||
movedEdges[e] = struct{}{}
|
||||
}
|
||||
|
||||
}
|
||||
q = append(q, curr.ChildrenArray...)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
|
@ -295,7 +437,36 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
|
|||
|
||||
// trace the edge to the specific shape's border
|
||||
points[startIndex] = shape.TraceToShapeBorder(srcShape, start, points[startIndex+1])
|
||||
points[endIndex] = shape.TraceToShapeBorder(dstShape, end, points[endIndex-1])
|
||||
|
||||
// if an edge to a container runs into its label, stop the edge at the label instead
|
||||
overlapsContainerLabel := false
|
||||
if edge.Dst.IsContainer() && edge.Dst.Attributes.Label.Value != "" {
|
||||
// assumes LabelPosition, LabelWidth, LabelHeight are all set if there is a label
|
||||
labelWidth := float64(*edge.Dst.LabelWidth)
|
||||
labelHeight := float64(*edge.Dst.LabelHeight)
|
||||
labelTL := label.Position(*edge.Dst.LabelPosition).
|
||||
GetPointOnBox(edge.Dst.Box, label.PADDING, labelWidth, labelHeight)
|
||||
|
||||
endingSegment := geo.Segment{Start: points[endIndex-1], End: points[endIndex]}
|
||||
labelBox := geo.NewBox(labelTL, labelWidth, labelHeight)
|
||||
// add left/right padding to box
|
||||
labelBox.TopLeft.X -= label.PADDING
|
||||
labelBox.Width += 2 * label.PADDING
|
||||
if intersections := labelBox.Intersections(endingSegment); len(intersections) > 0 {
|
||||
overlapsContainerLabel = true
|
||||
// move ending segment to label intersection point
|
||||
points[endIndex] = intersections[0]
|
||||
endingSegment.End = intersections[0]
|
||||
// if the segment becomes too short, just merge it with the previous segment
|
||||
if endIndex-1 > 0 && endingSegment.Length() < MIN_SEGMENT_LEN {
|
||||
points[endIndex-1] = points[endIndex]
|
||||
endIndex--
|
||||
}
|
||||
}
|
||||
}
|
||||
if !overlapsContainerLabel {
|
||||
points[endIndex] = shape.TraceToShapeBorder(dstShape, end, points[endIndex-1])
|
||||
}
|
||||
points = points[startIndex : endIndex+1]
|
||||
|
||||
// build a curved path from the dagre route
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
elk.js comes from https://github.com/kieler/elkjs
|
||||
Currently on v0.8.2
|
||||
|
||||
Attribution:
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import (
|
|||
_ "embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
|
|
@ -87,9 +88,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,
|
||||
}
|
||||
|
||||
|
|
@ -102,6 +103,9 @@ type elkOpts struct {
|
|||
ForceNodeModelOrder bool `json:"elk.layered.crossingMinimization.forceNodeModelOrder,omitempty"`
|
||||
ConsiderModelOrder string `json:"elk.layered.considerModelOrder.strategy,omitempty"`
|
||||
|
||||
NodeSizeConstraints string `json:"elk.nodeSize.constraints,omitempty"`
|
||||
NodeSizeMinimum string `json:"elk.nodeSize.minimum,omitempty"`
|
||||
|
||||
ConfigurableOpts
|
||||
}
|
||||
|
||||
|
|
@ -132,7 +136,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",
|
||||
|
|
@ -173,25 +177,29 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
|
|||
|
||||
walk(g.Root, nil, func(obj, parent *d2graph.Object) {
|
||||
height := obj.Height
|
||||
width := obj.Width
|
||||
if obj.LabelWidth != nil && obj.LabelHeight != nil {
|
||||
if obj.Attributes.Shape.Value == d2target.ShapeImage || obj.Attributes.Icon != nil {
|
||||
height += float64(*obj.LabelHeight) + label.PADDING
|
||||
}
|
||||
width = go2.Max(width, float64(*obj.LabelWidth))
|
||||
}
|
||||
|
||||
n := &ELKNode{
|
||||
ID: obj.AbsID(),
|
||||
Width: obj.Width,
|
||||
Width: width,
|
||||
Height: height,
|
||||
}
|
||||
|
||||
if len(obj.ChildrenArray) > 0 {
|
||||
n.LayoutOptions = &elkOpts{
|
||||
ForceNodeModelOrder: true,
|
||||
Thoroughness: 20,
|
||||
Thoroughness: 8,
|
||||
EdgeEdgeBetweenLayersSpacing: 50,
|
||||
HierarchyHandling: "INCLUDE_CHILDREN",
|
||||
ConsiderModelOrder: "NODES_AND_EDGES",
|
||||
// Why is it (height, width)? I have no clue, but it works.
|
||||
NodeSizeMinimum: fmt.Sprintf("(%d, %d)", int(math.Ceil(height)), int(math.Ceil(width))),
|
||||
ConfigurableOpts: ConfigurableOpts{
|
||||
NodeSpacing: opts.NodeSpacing,
|
||||
EdgeNodeSpacing: opts.EdgeNodeSpacing,
|
||||
|
|
@ -199,6 +207,30 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
|
|||
Padding: opts.Padding,
|
||||
},
|
||||
}
|
||||
// Only set if specified.
|
||||
// There's a bug where if it's the node label dimensions that set the NodeSizeMinimum,
|
||||
// then suddenly it's reversed back to (width, height). I must be missing something
|
||||
if obj.Attributes.Width != nil || obj.Attributes.Height != nil {
|
||||
n.LayoutOptions.NodeSizeConstraints = "MINIMUM_SIZE"
|
||||
}
|
||||
|
||||
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 +342,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
|
||||
|
|
|
|||
11
d2layouts/d2layoutfeatures/d2layoutfeatures.go
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
package d2layoutfeatures
|
||||
|
||||
// When this is true, objects can set ther `near` key to another object
|
||||
// When this is false, objects can only set `near` to constants
|
||||
const NEAR_OBJECT = "near_object"
|
||||
|
||||
// When this is true, containers can have dimensions set
|
||||
const CONTAINER_DIMENSIONS = "container_dimensions"
|
||||
|
||||
// When this is true, objects can specify their `top` and `left` keywords
|
||||
const TOP_LEFT = "top_left"
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -106,8 +106,11 @@ func layoutSequenceDiagram(g *d2graph.Graph, obj *d2graph.Object) (*sequenceDiag
|
|||
}
|
||||
}
|
||||
|
||||
sd := newSequenceDiagram(obj.ChildrenArray, edges)
|
||||
err := sd.layout()
|
||||
sd, err := newSequenceDiagram(obj.ChildrenArray, edges)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = sd.layout()
|
||||
return sd, err
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -407,8 +407,8 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package d2sequence
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"sort"
|
||||
|
|
@ -64,7 +65,7 @@ func getEdgeEarliestLineNum(e *d2graph.Edge) int {
|
|||
return min
|
||||
}
|
||||
|
||||
func newSequenceDiagram(objects []*d2graph.Object, messages []*d2graph.Edge) *sequenceDiagram {
|
||||
func newSequenceDiagram(objects []*d2graph.Object, messages []*d2graph.Edge) (*sequenceDiagram, error) {
|
||||
var actors []*d2graph.Object
|
||||
var groups []*d2graph.Object
|
||||
|
||||
|
|
@ -84,6 +85,10 @@ func newSequenceDiagram(objects []*d2graph.Object, messages []*d2graph.Edge) *se
|
|||
}
|
||||
}
|
||||
|
||||
if len(actors) == 0 {
|
||||
return nil, errors.New("no actors declared in sequence diagram")
|
||||
}
|
||||
|
||||
sd := &sequenceDiagram{
|
||||
messages: messages,
|
||||
actors: actors,
|
||||
|
|
@ -107,7 +112,7 @@ func newSequenceDiagram(objects []*d2graph.Object, messages []*d2graph.Edge) *se
|
|||
if actor.Width < MIN_ACTOR_WIDTH {
|
||||
dslShape := strings.ToLower(actor.Attributes.Shape.Value)
|
||||
switch dslShape {
|
||||
case d2target.ShapePerson, d2target.ShapeSquare, d2target.ShapeCircle:
|
||||
case d2target.ShapePerson, d2target.ShapeOval, d2target.ShapeSquare, d2target.ShapeCircle:
|
||||
// scale shape up to min width uniformly
|
||||
actor.Height *= MIN_ACTOR_WIDTH / actor.Width
|
||||
}
|
||||
|
|
@ -156,6 +161,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
|
||||
|
|
@ -176,7 +182,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
|
||||
|
|
@ -185,7 +190,7 @@ func newSequenceDiagram(objects []*d2graph.Object, messages []*d2graph.Edge) *se
|
|||
sd.maxActorHeight += float64(*sd.root.LabelHeight)
|
||||
}
|
||||
|
||||
return sd
|
||||
return sd, nil
|
||||
}
|
||||
|
||||
func (sd *sequenceDiagram) layout() error {
|
||||
|
|
@ -209,6 +214,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) {
|
||||
|
|
@ -246,8 +254,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.)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -273,6 +281,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.
|
||||
|
|
@ -354,7 +412,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
|
||||
|
||||
|
|
@ -363,8 +421,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.)
|
||||
|
|
@ -476,12 +536,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()+".")
|
||||
|
|
@ -499,7 +559,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),
|
||||
|
|
@ -526,7 +586,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)
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@ type CompileOptions struct {
|
|||
// - pre-measured (web setting)
|
||||
// TODO maybe some will want to configure code font too, but that's much lower priority
|
||||
FontFamily *d2fonts.FontFamily
|
||||
ThemeID int64
|
||||
}
|
||||
|
||||
func Compile(ctx context.Context, input string, opts *CompileOptions) (*d2target.Diagram, *d2graph.Graph, error) {
|
||||
|
|
@ -76,7 +75,7 @@ func compile(ctx context.Context, g *d2graph.Graph, opts *CompileOptions) (*d2ta
|
|||
}
|
||||
}
|
||||
|
||||
d, err := d2exporter.Export(ctx, g, opts.ThemeID, opts.FontFamily)
|
||||
d, err := d2exporter.Export(ctx, g, opts.FontFamily)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
|||
171
d2oracle/edit.go
|
|
@ -111,6 +111,25 @@ func _set(g *d2graph.Graph, key string, tag, value *string) error {
|
|||
toSkip := 1
|
||||
|
||||
reserved := false
|
||||
|
||||
// If you're setting `(x -> y)[0].style.opacity`
|
||||
// There's 3 cases you need to handle:
|
||||
// 1. The edge has no map.
|
||||
// 2. The edge has a style map with opacity not existing
|
||||
// 3. The edge has a style map with opacity existing
|
||||
//
|
||||
// How each case is handled:
|
||||
// 1. Append that mapkey to edge.
|
||||
// 2. Append opacity to the style map
|
||||
// 3. Set opacity
|
||||
//
|
||||
// There's certainly cleaner code to achieve this, but currently, there's a lot of logic to correctly scope, merge, append.
|
||||
// The tests should be comprehensive enough for a safe refactor someday
|
||||
//
|
||||
// reservedKey = "style"
|
||||
// reservedTargetKey = "opacity"
|
||||
reservedKey := ""
|
||||
reservedTargetKey := ""
|
||||
if mk.Key != nil {
|
||||
found := true
|
||||
for _, idel := range d2graph.Key(mk.Key) {
|
||||
|
|
@ -162,12 +181,14 @@ func _set(g *d2graph.Graph, key string, tag, value *string) error {
|
|||
}
|
||||
|
||||
attrs := obj.Attributes
|
||||
var edge *d2graph.Edge
|
||||
if len(mk.Edges) == 1 {
|
||||
if mk.EdgeIndex == nil {
|
||||
appendMapKey(scope, mk)
|
||||
return nil
|
||||
}
|
||||
edge, ok := obj.HasEdge(mk)
|
||||
var ok bool
|
||||
edge, ok = obj.HasEdge(mk)
|
||||
if !ok {
|
||||
return errors.New("edge not found")
|
||||
}
|
||||
|
|
@ -179,7 +200,17 @@ func _set(g *d2graph.Graph, key string, tag, value *string) error {
|
|||
// (y -> z)[0].style.animated: true
|
||||
if len(ref.MapKey.Edges) == 1 && ref.MapKey.EdgeIndex == nil {
|
||||
onlyInChain = false
|
||||
break
|
||||
}
|
||||
// If a ref has an exact match on this key, just change the value
|
||||
tmp1 := *ref.MapKey
|
||||
tmp2 := *mk
|
||||
noVal1 := &tmp1
|
||||
noVal2 := &tmp2
|
||||
noVal1.Value = d2ast.ValueBox{}
|
||||
noVal2.Value = d2ast.ValueBox{}
|
||||
if noVal1.Equals(noVal2) {
|
||||
ref.MapKey.Value = mk.Value
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if onlyInChain {
|
||||
|
|
@ -206,10 +237,26 @@ func _set(g *d2graph.Graph, key string, tag, value *string) error {
|
|||
if ref.MapKey.Value.Map != nil {
|
||||
foundMap = true
|
||||
scope = ref.MapKey.Value.Map
|
||||
// TODO when edges can have more fields, search for style
|
||||
if len(scope.Nodes) == 1 && scope.Nodes[0].MapKey.Value.Map != nil {
|
||||
scope = scope.Nodes[0].MapKey.Value.Map
|
||||
mk.Key.Path = mk.Key.Path[1:]
|
||||
for _, n := range scope.Nodes {
|
||||
if n.MapKey.Value.Map == nil {
|
||||
continue
|
||||
}
|
||||
if n.MapKey.Key == nil || len(n.MapKey.Key.Path) != 1 {
|
||||
continue
|
||||
}
|
||||
if n.MapKey.Key.Path[0].Unbox().ScalarString() == mk.Key.Path[toSkip-1].Unbox().ScalarString() {
|
||||
scope = n.MapKey.Value.Map
|
||||
if mk.Key.Path[0].Unbox().ScalarString() == "source-arrowhead" && edge.SrcArrowhead != nil {
|
||||
attrs = edge.SrcArrowhead
|
||||
}
|
||||
if mk.Key.Path[0].Unbox().ScalarString() == "target-arrowhead" && edge.DstArrowhead != nil {
|
||||
attrs = edge.DstArrowhead
|
||||
}
|
||||
reservedKey = mk.Key.Path[0].Unbox().ScalarString()
|
||||
mk.Key.Path = mk.Key.Path[1:]
|
||||
reservedTargetKey = mk.Key.Path[0].Unbox().ScalarString()
|
||||
break
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
|
@ -228,17 +275,79 @@ func _set(g *d2graph.Graph, key string, tag, value *string) error {
|
|||
if reserved {
|
||||
reservedIndex := toSkip - 1
|
||||
if mk.Key != nil && len(mk.Key.Path) > 0 {
|
||||
switch mk.Key.Path[reservedIndex].Unbox().ScalarString() {
|
||||
if reservedKey == "" {
|
||||
reservedKey = mk.Key.Path[reservedIndex].Unbox().ScalarString()
|
||||
}
|
||||
switch reservedKey {
|
||||
case "shape":
|
||||
if attrs.Shape.MapKey != nil {
|
||||
attrs.Shape.MapKey.SetScalar(mk.Value.ScalarBox())
|
||||
return nil
|
||||
}
|
||||
case "style":
|
||||
if len(mk.Key.Path[reservedIndex:]) != 2 {
|
||||
return errors.New("malformed style setting, expected 2 part path")
|
||||
case "link":
|
||||
if attrs.Link != nil && attrs.Link.MapKey != nil {
|
||||
attrs.Link.MapKey.SetScalar(mk.Value.ScalarBox())
|
||||
return nil
|
||||
}
|
||||
switch mk.Key.Path[reservedIndex+1].Unbox().ScalarString() {
|
||||
case "tooltip":
|
||||
if attrs.Tooltip != nil && attrs.Tooltip.MapKey != nil {
|
||||
attrs.Tooltip.MapKey.SetScalar(mk.Value.ScalarBox())
|
||||
return nil
|
||||
}
|
||||
case "width":
|
||||
if attrs.Width != nil && attrs.Width.MapKey != nil {
|
||||
attrs.Width.MapKey.SetScalar(mk.Value.ScalarBox())
|
||||
return nil
|
||||
}
|
||||
case "height":
|
||||
if attrs.Height != nil && attrs.Height.MapKey != nil {
|
||||
attrs.Height.MapKey.SetScalar(mk.Value.ScalarBox())
|
||||
return nil
|
||||
}
|
||||
case "top":
|
||||
if attrs.Top != nil && attrs.Top.MapKey != nil {
|
||||
attrs.Top.MapKey.SetScalar(mk.Value.ScalarBox())
|
||||
return nil
|
||||
}
|
||||
case "left":
|
||||
if attrs.Left != nil && attrs.Left.MapKey != nil {
|
||||
attrs.Left.MapKey.SetScalar(mk.Value.ScalarBox())
|
||||
return nil
|
||||
}
|
||||
case "source-arrowhead", "target-arrowhead":
|
||||
if reservedKey == "source-arrowhead" {
|
||||
attrs = edge.SrcArrowhead
|
||||
} else {
|
||||
attrs = edge.DstArrowhead
|
||||
}
|
||||
if attrs != nil {
|
||||
if reservedTargetKey == "" {
|
||||
if len(mk.Key.Path[reservedIndex:]) != 2 {
|
||||
return errors.New("malformed style setting, expected 2 part path")
|
||||
}
|
||||
reservedTargetKey = mk.Key.Path[reservedIndex+1].Unbox().ScalarString()
|
||||
}
|
||||
switch reservedTargetKey {
|
||||
case "shape":
|
||||
if attrs.Shape.MapKey != nil {
|
||||
attrs.Shape.MapKey.SetScalar(mk.Value.ScalarBox())
|
||||
return nil
|
||||
}
|
||||
case "label":
|
||||
if attrs.Label.MapKey != nil {
|
||||
attrs.Label.MapKey.SetScalar(mk.Value.ScalarBox())
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
case "style":
|
||||
if reservedTargetKey == "" {
|
||||
if len(mk.Key.Path[reservedIndex:]) != 2 {
|
||||
return errors.New("malformed style setting, expected 2 part path")
|
||||
}
|
||||
reservedTargetKey = mk.Key.Path[reservedIndex+1].Unbox().ScalarString()
|
||||
}
|
||||
switch reservedTargetKey {
|
||||
case "opacity":
|
||||
if attrs.Style.Opacity != nil {
|
||||
attrs.Style.Opacity.MapKey.SetScalar(mk.Value.ScalarBox())
|
||||
|
|
@ -667,6 +776,10 @@ func deleteReserved(g *d2graph.Graph, mk *d2ast.Key) (*d2graph.Graph, error) {
|
|||
if id == "near" ||
|
||||
id == "tooltip" ||
|
||||
id == "icon" ||
|
||||
id == "width" ||
|
||||
id == "height" ||
|
||||
id == "left" ||
|
||||
id == "top" ||
|
||||
id == "link" {
|
||||
err := deleteObjField(g, obj, id)
|
||||
if err != nil {
|
||||
|
|
@ -690,7 +803,9 @@ func deleteMapField(m *d2ast.Map, field string) {
|
|||
if n.MapKey != nil && n.MapKey.Key != nil {
|
||||
if n.MapKey.Key.Path[0].Unbox().ScalarString() == field {
|
||||
deleteFromMap(m, n.MapKey)
|
||||
} else if n.MapKey.Key.Path[0].Unbox().ScalarString() == "style" {
|
||||
} else if n.MapKey.Key.Path[0].Unbox().ScalarString() == "style" ||
|
||||
n.MapKey.Key.Path[0].Unbox().ScalarString() == "source-arrowhead" ||
|
||||
n.MapKey.Key.Path[0].Unbox().ScalarString() == "target-arrowhead" {
|
||||
if n.MapKey.Value.Map != nil {
|
||||
deleteMapField(n.MapKey.Value.Map, field)
|
||||
if len(n.MapKey.Value.Map.Nodes) == 0 {
|
||||
|
|
@ -773,13 +888,15 @@ 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 {
|
||||
ref.MapKey.Primary = ref.MapKey.Value.ScalarBox()
|
||||
} else if len(withoutSpecial) == 0 {
|
||||
hoistRefChildren(g, key, ref)
|
||||
deleteFromMap(ref.Scope, ref.MapKey)
|
||||
} else if ref.MapKey.Value.Unbox() == nil &&
|
||||
|
|
@ -1167,7 +1284,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
|
||||
}
|
||||
|
|
@ -1422,7 +1539,7 @@ func updateNear(prevG, g *d2graph.Graph, from, to *string) error {
|
|||
if len(n.MapKey.Key.Path) == 0 {
|
||||
continue
|
||||
}
|
||||
if n.MapKey.Key.Path[0].Unbox().ScalarString() == "near" {
|
||||
if n.MapKey.Key.Path[len(n.MapKey.Key.Path)-1].Unbox().ScalarString() == "near" {
|
||||
k := n.MapKey.Value.ScalarBox().Unbox().ScalarString()
|
||||
if strings.EqualFold(k, *from) && to == nil {
|
||||
deleteFromMap(obj.Map, n.MapKey)
|
||||
|
|
@ -1839,6 +1956,12 @@ func DeleteIDDeltas(g *d2graph.Graph, key string) (deltas map[string]string, err
|
|||
conflictNewIDs := make(map[*d2graph.Object]string)
|
||||
conflictOldIDs := make(map[*d2graph.Object]string)
|
||||
if mk.Key != nil {
|
||||
ida := d2graph.Key(mk.Key)
|
||||
// Deleting a reserved field cannot possibly have any deltas
|
||||
if _, ok := d2graph.ReservedKeywords[ida[len(ida)-1]]; ok {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var ok bool
|
||||
obj, ok = g.Root.HasChild(d2graph.Key(mk.Key))
|
||||
if !ok {
|
||||
|
|
@ -2066,7 +2189,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
|
||||
|
|
@ -2080,11 +2209,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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -695,6 +695,151 @@ square.style.opacity: 0.2
|
|||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "set_position",
|
||||
text: `square
|
||||
`,
|
||||
key: `square.top`,
|
||||
value: go2.Pointer(`200`),
|
||||
exp: `square: {top: 200}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "replace_position",
|
||||
text: `square: {
|
||||
width: 100
|
||||
top: 32
|
||||
left: 44
|
||||
}
|
||||
`,
|
||||
key: `square.top`,
|
||||
value: go2.Pointer(`200`),
|
||||
exp: `square: {
|
||||
width: 100
|
||||
top: 200
|
||||
left: 44
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "set_dimensions",
|
||||
text: `square
|
||||
`,
|
||||
key: `square.width`,
|
||||
value: go2.Pointer(`200`),
|
||||
exp: `square: {width: 200}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "replace_dimensions",
|
||||
text: `square: {
|
||||
width: 100
|
||||
}
|
||||
`,
|
||||
key: `square.width`,
|
||||
value: go2.Pointer(`200`),
|
||||
exp: `square: {
|
||||
width: 200
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "set_tooltip",
|
||||
text: `square
|
||||
`,
|
||||
key: `square.tooltip`,
|
||||
value: go2.Pointer(`y`),
|
||||
exp: `square: {tooltip: y}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "replace_tooltip",
|
||||
text: `square: {
|
||||
tooltip: x
|
||||
}
|
||||
`,
|
||||
key: `square.tooltip`,
|
||||
value: go2.Pointer(`y`),
|
||||
exp: `square: {
|
||||
tooltip: y
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "replace_link",
|
||||
text: `square: {
|
||||
link: https://google.com
|
||||
}
|
||||
`,
|
||||
key: `square.link`,
|
||||
value: go2.Pointer(`https://apple.com`),
|
||||
exp: `square: {
|
||||
link: https://apple.com
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "replace_arrowhead",
|
||||
text: `x -> y: {
|
||||
target-arrowhead.shape: diamond
|
||||
}
|
||||
`,
|
||||
key: `(x -> y)[0].target-arrowhead.shape`,
|
||||
value: go2.Pointer(`circle`),
|
||||
exp: `x -> y: {
|
||||
target-arrowhead.shape: circle
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "replace_arrowhead_map",
|
||||
text: `x -> y: {
|
||||
target-arrowhead: {
|
||||
shape: diamond
|
||||
}
|
||||
}
|
||||
`,
|
||||
key: `(x -> y)[0].target-arrowhead.shape`,
|
||||
value: go2.Pointer(`circle`),
|
||||
exp: `x -> y: {
|
||||
target-arrowhead: {
|
||||
shape: circle
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "replace_edge_style_map",
|
||||
text: `x -> y: {
|
||||
style: {
|
||||
stroke-dash: 3
|
||||
}
|
||||
}
|
||||
`,
|
||||
key: `(x -> y)[0].style.stroke-dash`,
|
||||
value: go2.Pointer(`4`),
|
||||
exp: `x -> y: {
|
||||
style: {
|
||||
stroke-dash: 4
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "replace_edge_style",
|
||||
text: `x -> y: {
|
||||
style.stroke-width: 1
|
||||
style.stroke-dash: 4
|
||||
}
|
||||
`,
|
||||
key: `(x -> y)[0].style.stroke-dash`,
|
||||
value: go2.Pointer(`3`),
|
||||
exp: `x -> y: {
|
||||
style.stroke-width: 1
|
||||
style.stroke-dash: 3
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "label_unset",
|
||||
text: `square: "Always try to do things in chronological order; it's less confusing that way."
|
||||
|
|
@ -1024,6 +1169,57 @@ a.b -> a.c: {style.animated: true}
|
|||
value: go2.Pointer(`true`),
|
||||
|
||||
exp: `x -> y: {style.animated: true}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "edge_set_arrowhead",
|
||||
text: `x -> y
|
||||
`,
|
||||
key: `(x -> y)[0].target-arrowhead.shape`,
|
||||
value: go2.Pointer(`diamond`),
|
||||
|
||||
exp: `x -> y: {target-arrowhead.shape: diamond}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "edge_replace_arrowhead",
|
||||
text: `x -> y: {target-arrowhead.shape: circle}
|
||||
`,
|
||||
key: `(x -> y)[0].target-arrowhead.shape`,
|
||||
value: go2.Pointer(`diamond`),
|
||||
|
||||
exp: `x -> y: {target-arrowhead.shape: diamond}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "edge_replace_arrowhead_indexed",
|
||||
text: `x -> y
|
||||
(x -> y)[0].target-arrowhead.shape: circle
|
||||
`,
|
||||
key: `(x -> y)[0].target-arrowhead.shape`,
|
||||
value: go2.Pointer(`diamond`),
|
||||
|
||||
exp: `x -> y
|
||||
(x -> y)[0].target-arrowhead.shape: diamond
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "edge_merge_arrowhead",
|
||||
text: `x -> y: {
|
||||
target-arrowhead: {
|
||||
label: 1
|
||||
}
|
||||
}
|
||||
`,
|
||||
key: `(x -> y)[0].target-arrowhead.shape`,
|
||||
value: go2.Pointer(`diamond`),
|
||||
|
||||
exp: `x -> y: {
|
||||
target-arrowhead: {
|
||||
label: 1
|
||||
shape: diamond
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
|
|
@ -1043,6 +1239,30 @@ a.b -> a.c: {style.animated: true}
|
|||
animated: true
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "edge_flat_merge_arrowhead",
|
||||
text: `x -> y -> z
|
||||
(x -> y)[0].target-arrowhead.shape: diamond
|
||||
`,
|
||||
key: `(x -> y)[0].target-arrowhead.shape`,
|
||||
value: go2.Pointer(`circle`),
|
||||
|
||||
exp: `x -> y -> z
|
||||
(x -> y)[0].target-arrowhead.shape: circle
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "edge_index_merge_style",
|
||||
text: `x -> y -> z
|
||||
(x -> y)[0].style.opacity: 0.4
|
||||
`,
|
||||
key: `(x -> y)[0].style.opacity`,
|
||||
value: go2.Pointer(`0.5`),
|
||||
|
||||
exp: `x -> y -> z
|
||||
(x -> y)[0].style.opacity: 0.5
|
||||
`,
|
||||
},
|
||||
{
|
||||
|
|
@ -1755,6 +1975,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 +2221,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",
|
||||
|
||||
|
|
@ -2162,7 +2440,7 @@ a.b.c: {
|
|||
`,
|
||||
},
|
||||
{
|
||||
name: "near",
|
||||
name: "invalid-near",
|
||||
|
||||
text: `x: {
|
||||
near: y
|
||||
|
|
@ -2176,6 +2454,33 @@ y
|
|||
near: x.y
|
||||
y
|
||||
}
|
||||
`,
|
||||
expErr: `failed to move: "y" to "x.y": failed to recompile:
|
||||
x: {
|
||||
near: x.y
|
||||
y
|
||||
}
|
||||
|
||||
d2/testdata/d2oracle/TestMove/invalid-near.d2:2:9: near keys cannot be set to an descendant`,
|
||||
},
|
||||
{
|
||||
name: "near",
|
||||
|
||||
text: `x: {
|
||||
near: y
|
||||
}
|
||||
a
|
||||
y
|
||||
`,
|
||||
key: `y`,
|
||||
newKey: `a.y`,
|
||||
|
||||
exp: `x: {
|
||||
near: a.y
|
||||
}
|
||||
a: {
|
||||
y
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
|
|
@ -3004,6 +3309,22 @@ d
|
|||
exp: `a: {
|
||||
_.b -> c -> _.b
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "container_multiple_refs_with_underscore",
|
||||
|
||||
text: `a
|
||||
b: {
|
||||
_.a
|
||||
}
|
||||
`,
|
||||
key: `a`,
|
||||
newKey: `b.a`,
|
||||
|
||||
exp: `b: {
|
||||
a
|
||||
}
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
|
@ -3149,6 +3470,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
|
||||
`,
|
||||
},
|
||||
{
|
||||
|
|
@ -3291,6 +3647,71 @@ x
|
|||
key: `x`,
|
||||
|
||||
exp: `y
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "arrowhead",
|
||||
|
||||
text: `x -> y: {
|
||||
target-arrowhead.shape: diamond
|
||||
}
|
||||
`,
|
||||
key: `(x -> y)[0].target-arrowhead`,
|
||||
|
||||
exp: `x -> y
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "arrowhead_shape",
|
||||
|
||||
text: `x -> y: {
|
||||
target-arrowhead.shape: diamond
|
||||
}
|
||||
`,
|
||||
key: `(x -> y)[0].target-arrowhead.shape`,
|
||||
|
||||
exp: `x -> y
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "arrowhead_label",
|
||||
|
||||
text: `x -> y: {
|
||||
target-arrowhead.shape: diamond
|
||||
target-arrowhead.label: 1
|
||||
}
|
||||
`,
|
||||
key: `(x -> y)[0].target-arrowhead.label`,
|
||||
|
||||
exp: `x -> y: {
|
||||
target-arrowhead.shape: diamond
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "arrowhead_map",
|
||||
|
||||
text: `x -> y: {
|
||||
target-arrowhead: {
|
||||
shape: diamond
|
||||
}
|
||||
}
|
||||
`,
|
||||
key: `(x -> y)[0].target-arrowhead.shape`,
|
||||
|
||||
exp: `x -> y
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "edge-only-style",
|
||||
|
||||
text: `x -> y: {
|
||||
style.stroke: red
|
||||
}
|
||||
`,
|
||||
key: `(x -> y)[0].style.stroke`,
|
||||
|
||||
exp: `x -> y
|
||||
`,
|
||||
},
|
||||
{
|
||||
|
|
@ -3419,6 +3840,37 @@ y
|
|||
|
||||
exp: `x
|
||||
y
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "delete_container_of_near",
|
||||
|
||||
text: `direction: down
|
||||
first input -> start game -> game loop
|
||||
|
||||
game loop: {
|
||||
direction: down
|
||||
input -> increase bird top velocity
|
||||
|
||||
move bird -> move pipes -> render
|
||||
|
||||
render -> no collision -> wait 16 milliseconds -> move bird
|
||||
render -> collision detected -> game over
|
||||
no collision.near: game loop.collision detected
|
||||
}
|
||||
`,
|
||||
key: `game loop`,
|
||||
|
||||
exp: `direction: down
|
||||
first input -> start game
|
||||
|
||||
input -> increase bird top velocity
|
||||
|
||||
move bird -> move pipes -> render
|
||||
|
||||
render -> no collision -> wait 16 milliseconds -> move bird
|
||||
render -> collision detected -> game over
|
||||
no collision.near: collision detected
|
||||
`,
|
||||
},
|
||||
{
|
||||
|
|
@ -4339,6 +4791,64 @@ A -> B: {style.stroke: "#2b50c2"}
|
|||
exp: `A: {style.stroke: "#000e3d"}
|
||||
B
|
||||
A -> B
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "width",
|
||||
|
||||
text: `x: {
|
||||
width: 200
|
||||
}
|
||||
`,
|
||||
key: `x.width`,
|
||||
|
||||
exp: `x
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "left",
|
||||
|
||||
text: `x: {
|
||||
left: 200
|
||||
}
|
||||
`,
|
||||
key: `x.left`,
|
||||
|
||||
exp: `x
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "chaos_1",
|
||||
|
||||
text: `cm: {shape: cylinder}
|
||||
cm <-> cm: {source-arrowhead.shape: cf-one-required}
|
||||
mt: z
|
||||
cdpdxz
|
||||
|
||||
bymdyk: hdzuj {shape: class}
|
||||
|
||||
bymdyk <-> bymdyk
|
||||
cm
|
||||
|
||||
cm <-> bymdyk: {
|
||||
source-arrowhead.shape: cf-many-required
|
||||
target-arrowhead.shape: arrow
|
||||
}
|
||||
bymdyk <-> cdpdxz
|
||||
|
||||
bymdyk -> cm: nk {
|
||||
target-arrowhead.shape: diamond
|
||||
target-arrowhead.label: 1
|
||||
}
|
||||
`,
|
||||
key: `bymdyk`,
|
||||
|
||||
exp: `cm: {shape: cylinder}
|
||||
cm <-> cm: {source-arrowhead.shape: cf-one-required}
|
||||
mt: z
|
||||
cdpdxz
|
||||
|
||||
cm
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
|
@ -4701,6 +5211,46 @@ x.y.z.w.e.p.l -> x.y.z.1.2.3.4
|
|||
|
||||
exp: `{
|
||||
"x.x": "x"
|
||||
}`,
|
||||
},
|
||||
{
|
||||
name: "nested-height",
|
||||
|
||||
text: `x: {
|
||||
a -> b
|
||||
height: 200
|
||||
}
|
||||
`,
|
||||
key: `x.height`,
|
||||
|
||||
exp: `null`,
|
||||
},
|
||||
{
|
||||
name: "edge-style",
|
||||
|
||||
text: `x <-> y: {
|
||||
target-arrowhead: circle
|
||||
source-arrowhead: diamond
|
||||
}
|
||||
`,
|
||||
key: `(x <-> y)[0].target-arrowhead`,
|
||||
|
||||
exp: `null`,
|
||||
},
|
||||
{
|
||||
name: "only-reserved",
|
||||
text: `guitar: {
|
||||
books: {
|
||||
_._.pipe: {
|
||||
a
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
key: `pipe`,
|
||||
|
||||
exp: `{
|
||||
"pipe.a": "a"
|
||||
}`,
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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,11 +71,14 @@ 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.
|
||||
Path string `json:"path"`
|
||||
|
||||
Features []PluginFeature `json:"features"`
|
||||
}
|
||||
|
||||
const binaryPrefix = "d2plugin-"
|
||||
|
|
@ -122,12 +125,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,6 +66,8 @@ func (p dagrePlugin) Info(ctx context.Context) (*PluginInfo, error) {
|
|||
|
||||
return &PluginInfo{
|
||||
Name: "dagre",
|
||||
Type: "bundled",
|
||||
Features: []PluginFeature{},
|
||||
ShortHelp: "The directed graph layout library Dagre",
|
||||
LongHelp: fmt.Sprintf(`dagre is a directed graph layout library for JavaScript.
|
||||
See https://d2lang.com/tour/dagre for more.
|
||||
|
|
|
|||
|
|
@ -85,7 +85,12 @@ func (p elkPlugin) Info(ctx context.Context) (*PluginInfo, error) {
|
|||
f.AddToOpts(opts)
|
||||
}
|
||||
return &PluginInfo{
|
||||
Name: "elk",
|
||||
Name: "elk",
|
||||
Type: "bundled",
|
||||
Features: []PluginFeature{
|
||||
CONTAINER_DIMENSIONS,
|
||||
DESCENDANT_EDGES,
|
||||
},
|
||||
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.
|
||||
|
|
|
|||
74
d2plugin/plugin_features.go
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
package d2plugin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"oss.terrastruct.com/d2/d2graph"
|
||||
)
|
||||
|
||||
type PluginFeature string
|
||||
|
||||
// When this is true, objects can set ther `near` key to another object
|
||||
// When this is false, objects can only set `near` to constants
|
||||
const NEAR_OBJECT PluginFeature = "near_object"
|
||||
|
||||
// When this is true, containers can have dimensions set
|
||||
const CONTAINER_DIMENSIONS PluginFeature = "container_dimensions"
|
||||
|
||||
// When this is true, objects can specify their `top` and `left` keywords
|
||||
const TOP_LEFT PluginFeature = "top_left"
|
||||
|
||||
// When this is true, containers can have connections to descendants
|
||||
const DESCENDANT_EDGES PluginFeature = "descendant_edges"
|
||||
|
||||
func FeatureSupportCheck(info *PluginInfo, g *d2graph.Graph) error {
|
||||
// Older version of plugin. Skip checking.
|
||||
if info.Features == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
featureMap := make(map[PluginFeature]struct{}, len(info.Features))
|
||||
for _, f := range info.Features {
|
||||
featureMap[f] = struct{}{}
|
||||
}
|
||||
|
||||
for _, obj := range g.Objects {
|
||||
if obj.Attributes.Top != nil || obj.Attributes.Left != nil {
|
||||
if _, ok := featureMap[TOP_LEFT]; !ok {
|
||||
return fmt.Errorf(`Object "%s" has attribute "top" and/or "left" set, but layout engine "%s" does not support locked positions.`, obj.AbsID(), info.Name)
|
||||
}
|
||||
}
|
||||
if (obj.Attributes.Width != nil || obj.Attributes.Height != nil) && len(obj.ChildrenArray) > 0 {
|
||||
if _, ok := featureMap[CONTAINER_DIMENSIONS]; !ok {
|
||||
return fmt.Errorf(`Object "%s" has attribute "width" and/or "height" set, but layout engine "%s" does not support dimensions set on containers.`, obj.AbsID(), info.Name)
|
||||
}
|
||||
}
|
||||
|
||||
if obj.Attributes.NearKey != nil {
|
||||
_, isKey := g.Root.HasChild(d2graph.Key(obj.Attributes.NearKey))
|
||||
if isKey {
|
||||
if _, ok := featureMap[NEAR_OBJECT]; !ok {
|
||||
return fmt.Errorf(`Object "%s" has "near" set to another object, but layout engine "%s" only supports constant values for "near".`, obj.AbsID(), info.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if _, ok := featureMap[DESCENDANT_EDGES]; !ok {
|
||||
for _, e := range g.Edges {
|
||||
// descendant edges are ok in sequence diagrams
|
||||
if e.Src.OuterSequenceDiagram() != nil || e.Dst.OuterSequenceDiagram() != nil {
|
||||
continue
|
||||
}
|
||||
if !e.Src.IsContainer() && !e.Dst.IsContainer() {
|
||||
continue
|
||||
}
|
||||
if e.Src == e.Dst {
|
||||
return fmt.Errorf(`Connection "%s" is a self loop on a container, but layout engine "%s" does not support this.`, e.AbsID(), info.Name)
|
||||
}
|
||||
if e.Src.IsDescendantOf(e.Dst) || e.Dst.IsDescendantOf(e.Src) {
|
||||
return fmt.Errorf(`Connection "%s" goes from a container to a descendant, but layout engine "%s" does not support this.`, e.AbsID(), info.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package d2sketch
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
|
@ -11,21 +12,23 @@ import (
|
|||
"github.com/dop251/goja"
|
||||
|
||||
"oss.terrastruct.com/d2/d2target"
|
||||
"oss.terrastruct.com/d2/d2themes"
|
||||
"oss.terrastruct.com/d2/lib/color"
|
||||
"oss.terrastruct.com/d2/lib/geo"
|
||||
"oss.terrastruct.com/d2/lib/label"
|
||||
"oss.terrastruct.com/d2/lib/svg"
|
||||
"oss.terrastruct.com/util-go/go2"
|
||||
)
|
||||
|
||||
//go:embed fillpattern.svg
|
||||
var fillPattern string
|
||||
|
||||
//go:embed rough.js
|
||||
var roughJS string
|
||||
|
||||
//go:embed setup.js
|
||||
var setupJS string
|
||||
|
||||
//go:embed streaks.txt
|
||||
var streaks string
|
||||
|
||||
type Runner goja.Runtime
|
||||
|
||||
var baseRoughProps = `fillWeight: 2.0,
|
||||
|
|
@ -36,6 +39,11 @@ seed: 1,`
|
|||
|
||||
var floatRE = regexp.MustCompile(`(\d+)\.(\d+)`)
|
||||
|
||||
const (
|
||||
BG_COLOR = color.N7
|
||||
FG_COLOR = color.N1
|
||||
)
|
||||
|
||||
func (r *Runner) run(js string) (goja.Value, error) {
|
||||
vm := (*goja.Runtime)(r)
|
||||
return vm.RunString(js)
|
||||
|
|
@ -53,123 +61,173 @@ func InitSketchVM() (*Runner, error) {
|
|||
return &r, nil
|
||||
}
|
||||
|
||||
// DefineFillPattern adds a reusable pattern that is overlayed on shapes with
|
||||
// DefineFillPatterns adds reusable patterns that are overlayed on shapes with
|
||||
// fill. This gives it a subtle streaky effect that subtly looks hand-drawn but
|
||||
// not distractingly so.
|
||||
func DefineFillPattern() string {
|
||||
return fmt.Sprintf(`<defs>
|
||||
<pattern id="streaks"
|
||||
x="0" y="0" width="100" height="100"
|
||||
patternUnits="userSpaceOnUse" >
|
||||
%s
|
||||
</pattern>
|
||||
</defs>`, fillPattern)
|
||||
func DefineFillPatterns(buf *bytes.Buffer) {
|
||||
source := buf.String()
|
||||
fmt.Fprint(buf, "<defs>")
|
||||
|
||||
defineFillPattern(buf, source, "bright", "rgba(0, 0, 0, 0.1)")
|
||||
defineFillPattern(buf, source, "normal", "rgba(0, 0, 0, 0.16)")
|
||||
defineFillPattern(buf, source, "dark", "rgba(0, 0, 0, 0.32)")
|
||||
defineFillPattern(buf, source, "darker", "rgba(255, 255, 255, 0.24)")
|
||||
|
||||
fmt.Fprint(buf, "</defs>")
|
||||
}
|
||||
|
||||
func defineFillPattern(buf *bytes.Buffer, source string, luminanceCategory, fill string) {
|
||||
trigger := fmt.Sprintf(`url(#streaks-%s)`, luminanceCategory)
|
||||
if strings.Contains(source, trigger) {
|
||||
fmt.Fprintf(buf, streaks, luminanceCategory, fill)
|
||||
}
|
||||
}
|
||||
|
||||
func Rect(r *Runner, shape d2target.Shape) (string, error) {
|
||||
js := fmt.Sprintf(`node = rc.rectangle(0, 0, %d, %d, {
|
||||
fill: "%s",
|
||||
stroke: "%s",
|
||||
fill: "#000",
|
||||
stroke: "#000",
|
||||
strokeWidth: %d,
|
||||
%s
|
||||
});`, shape.Width, shape.Height, shape.Fill, shape.Stroke, shape.StrokeWidth, baseRoughProps)
|
||||
});`, shape.Width, shape.Height, shape.StrokeWidth, baseRoughProps)
|
||||
paths, err := computeRoughPathData(r, js)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
output := ""
|
||||
pathEl := d2themes.NewThemableElement("path")
|
||||
pathEl.SetTranslate(float64(shape.Pos.X), float64(shape.Pos.Y))
|
||||
pathEl.Fill, pathEl.Stroke = d2themes.ShapeTheme(shape)
|
||||
pathEl.ClassName = "shape"
|
||||
pathEl.Style = shape.CSSStyle()
|
||||
for _, p := range paths {
|
||||
output += fmt.Sprintf(
|
||||
`<path class="shape" transform="translate(%d %d)" d="%s" style="%s" />`,
|
||||
shape.Pos.X, shape.Pos.Y, p, shape.CSSStyle(),
|
||||
)
|
||||
pathEl.D = p
|
||||
output += pathEl.Render()
|
||||
}
|
||||
output += fmt.Sprintf(
|
||||
`<rect class="sketch-overlay" transform="translate(%d %d)" width="%d" height="%d" />`,
|
||||
shape.Pos.X, shape.Pos.Y, shape.Width, shape.Height,
|
||||
)
|
||||
|
||||
sketchOEl := d2themes.NewThemableElement("rect")
|
||||
sketchOEl.SetTranslate(float64(shape.Pos.X), float64(shape.Pos.Y))
|
||||
sketchOEl.Width = float64(shape.Width)
|
||||
sketchOEl.Height = float64(shape.Height)
|
||||
renderedSO, err := d2themes.NewThemableSketchOverlay(sketchOEl, pathEl.Fill).Render()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
output += renderedSO
|
||||
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func DoubleRect(r *Runner, shape d2target.Shape) (string, error) {
|
||||
jsBigRect := fmt.Sprintf(`node = rc.rectangle(0, 0, %d, %d, {
|
||||
fill: "%s",
|
||||
stroke: "%s",
|
||||
fill: "#000",
|
||||
stroke: "#000",
|
||||
strokeWidth: %d,
|
||||
%s
|
||||
});`, shape.Width, shape.Height, shape.Fill, shape.Stroke, shape.StrokeWidth, baseRoughProps)
|
||||
});`, shape.Width, shape.Height, shape.StrokeWidth, baseRoughProps)
|
||||
pathsBigRect, err := computeRoughPathData(r, jsBigRect)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
jsSmallRect := fmt.Sprintf(`node = rc.rectangle(0, 0, %d, %d, {
|
||||
fill: "%s",
|
||||
stroke: "%s",
|
||||
fill: "#000",
|
||||
stroke: "#000",
|
||||
strokeWidth: %d,
|
||||
%s
|
||||
});`, shape.Width-d2target.INNER_BORDER_OFFSET*2, shape.Height-d2target.INNER_BORDER_OFFSET*2, shape.Fill, shape.Stroke, shape.StrokeWidth, baseRoughProps)
|
||||
});`, shape.Width-d2target.INNER_BORDER_OFFSET*2, shape.Height-d2target.INNER_BORDER_OFFSET*2, shape.StrokeWidth, baseRoughProps)
|
||||
pathsSmallRect, err := computeRoughPathData(r, jsSmallRect)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
output := ""
|
||||
|
||||
pathEl := d2themes.NewThemableElement("path")
|
||||
pathEl.SetTranslate(float64(shape.Pos.X), float64(shape.Pos.Y))
|
||||
pathEl.Fill, pathEl.Stroke = d2themes.ShapeTheme(shape)
|
||||
pathEl.ClassName = "shape"
|
||||
pathEl.Style = shape.CSSStyle()
|
||||
for _, p := range pathsBigRect {
|
||||
output += fmt.Sprintf(
|
||||
`<path class="shape" transform="translate(%d %d)" d="%s" style="%s" />`,
|
||||
shape.Pos.X, shape.Pos.Y, p, shape.CSSStyle(),
|
||||
)
|
||||
pathEl.D = p
|
||||
output += pathEl.Render()
|
||||
}
|
||||
|
||||
pathEl = d2themes.NewThemableElement("path")
|
||||
pathEl.SetTranslate(float64(shape.Pos.X+d2target.INNER_BORDER_OFFSET), float64(shape.Pos.Y+d2target.INNER_BORDER_OFFSET))
|
||||
pathEl.Fill, pathEl.Stroke = d2themes.ShapeTheme(shape)
|
||||
// No need for inner to double paint
|
||||
pathEl.Fill = "transparent"
|
||||
pathEl.ClassName = "shape"
|
||||
pathEl.Style = shape.CSSStyle()
|
||||
for _, p := range pathsSmallRect {
|
||||
output += fmt.Sprintf(
|
||||
`<path class="shape" transform="translate(%d %d)" d="%s" style="%s" />`,
|
||||
shape.Pos.X+d2target.INNER_BORDER_OFFSET, shape.Pos.Y+d2target.INNER_BORDER_OFFSET, p, shape.CSSStyle(),
|
||||
)
|
||||
pathEl.D = p
|
||||
output += pathEl.Render()
|
||||
}
|
||||
output += fmt.Sprintf(
|
||||
`<rect class="sketch-overlay" transform="translate(%d %d)" width="%d" height="%d" />`,
|
||||
shape.Pos.X, shape.Pos.Y, shape.Width, shape.Height,
|
||||
)
|
||||
|
||||
sketchOEl := d2themes.NewThemableElement("rect")
|
||||
sketchOEl.SetTranslate(float64(shape.Pos.X), float64(shape.Pos.Y))
|
||||
sketchOEl.Width = float64(shape.Width)
|
||||
sketchOEl.Height = float64(shape.Height)
|
||||
renderedSO, err := d2themes.NewThemableSketchOverlay(sketchOEl, shape.Fill).Render()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
output += renderedSO
|
||||
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func Oval(r *Runner, shape d2target.Shape) (string, error) {
|
||||
js := fmt.Sprintf(`node = rc.ellipse(%d, %d, %d, %d, {
|
||||
fill: "%s",
|
||||
stroke: "%s",
|
||||
fill: "#000",
|
||||
stroke: "#000",
|
||||
strokeWidth: %d,
|
||||
%s
|
||||
});`, shape.Width/2, shape.Height/2, shape.Width, shape.Height, shape.Fill, shape.Stroke, shape.StrokeWidth, baseRoughProps)
|
||||
});`, shape.Width/2, shape.Height/2, shape.Width, shape.Height, shape.StrokeWidth, baseRoughProps)
|
||||
paths, err := computeRoughPathData(r, js)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
output := ""
|
||||
pathEl := d2themes.NewThemableElement("path")
|
||||
pathEl.SetTranslate(float64(shape.Pos.X), float64(shape.Pos.Y))
|
||||
pathEl.Fill, pathEl.Stroke = d2themes.ShapeTheme(shape)
|
||||
pathEl.ClassName = "shape"
|
||||
pathEl.Style = shape.CSSStyle()
|
||||
for _, p := range paths {
|
||||
output += fmt.Sprintf(
|
||||
`<path class="shape" transform="translate(%d %d)" d="%s" style="%s" />`,
|
||||
shape.Pos.X, shape.Pos.Y, p, shape.CSSStyle(),
|
||||
)
|
||||
pathEl.D = p
|
||||
output += pathEl.Render()
|
||||
}
|
||||
output += fmt.Sprintf(
|
||||
`<ellipse class="sketch-overlay" transform="translate(%d %d)" rx="%d" ry="%d" />`,
|
||||
shape.Pos.X+shape.Width/2, shape.Pos.Y+shape.Height/2, shape.Width/2, shape.Height/2,
|
||||
)
|
||||
|
||||
soElement := d2themes.NewThemableElement("ellipse")
|
||||
soElement.SetTranslate(float64(shape.Pos.X+shape.Width/2), float64(shape.Pos.Y+shape.Height/2))
|
||||
soElement.Rx = float64(shape.Width / 2)
|
||||
soElement.Ry = float64(shape.Height / 2)
|
||||
renderedSO, err := d2themes.NewThemableSketchOverlay(
|
||||
soElement,
|
||||
pathEl.Fill,
|
||||
).Render()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
output += renderedSO
|
||||
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func DoubleOval(r *Runner, shape d2target.Shape) (string, error) {
|
||||
jsBigCircle := fmt.Sprintf(`node = rc.ellipse(%d, %d, %d, %d, {
|
||||
fill: "%s",
|
||||
stroke: "%s",
|
||||
fill: "#000",
|
||||
stroke: "#000",
|
||||
strokeWidth: %d,
|
||||
%s
|
||||
});`, shape.Width/2, shape.Height/2, shape.Width, shape.Height, shape.Fill, shape.Stroke, shape.StrokeWidth, baseRoughProps)
|
||||
});`, shape.Width/2, shape.Height/2, shape.Width, shape.Height, shape.StrokeWidth, baseRoughProps)
|
||||
jsSmallCircle := fmt.Sprintf(`node = rc.ellipse(%d, %d, %d, %d, {
|
||||
fill: "%s",
|
||||
stroke: "%s",
|
||||
fill: "#000",
|
||||
stroke: "#000",
|
||||
strokeWidth: %d,
|
||||
%s
|
||||
});`, shape.Width/2, shape.Height/2, shape.Width-d2target.INNER_BORDER_OFFSET*2, shape.Height-d2target.INNER_BORDER_OFFSET*2, shape.Fill, shape.Stroke, shape.StrokeWidth, baseRoughProps)
|
||||
});`, shape.Width/2, shape.Height/2, shape.Width-d2target.INNER_BORDER_OFFSET*2, shape.Height-d2target.INNER_BORDER_OFFSET*2, shape.StrokeWidth, baseRoughProps)
|
||||
pathsBigCircle, err := computeRoughPathData(r, jsBigCircle)
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
|
@ -178,23 +236,43 @@ func DoubleOval(r *Runner, shape d2target.Shape) (string, error) {
|
|||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
output := ""
|
||||
|
||||
pathEl := d2themes.NewThemableElement("path")
|
||||
pathEl.SetTranslate(float64(shape.Pos.X), float64(shape.Pos.Y))
|
||||
pathEl.Fill, pathEl.Stroke = d2themes.ShapeTheme(shape)
|
||||
pathEl.ClassName = "shape"
|
||||
pathEl.Style = shape.CSSStyle()
|
||||
for _, p := range pathsBigCircle {
|
||||
output += fmt.Sprintf(
|
||||
`<path class="shape" transform="translate(%d %d)" d="%s" style="%s" />`,
|
||||
shape.Pos.X, shape.Pos.Y, p, shape.CSSStyle(),
|
||||
)
|
||||
pathEl.D = p
|
||||
output += pathEl.Render()
|
||||
}
|
||||
|
||||
pathEl = d2themes.NewThemableElement("path")
|
||||
pathEl.SetTranslate(float64(shape.Pos.X), float64(shape.Pos.Y))
|
||||
pathEl.Fill, pathEl.Stroke = d2themes.ShapeTheme(shape)
|
||||
// No need for inner to double paint
|
||||
pathEl.Fill = "transparent"
|
||||
pathEl.ClassName = "shape"
|
||||
pathEl.Style = shape.CSSStyle()
|
||||
for _, p := range pathsSmallCircle {
|
||||
output += fmt.Sprintf(
|
||||
`<path class="shape" transform="translate(%d %d)" d="%s" style="%s" />`,
|
||||
shape.Pos.X, shape.Pos.Y, p, shape.CSSStyle(),
|
||||
)
|
||||
pathEl.D = p
|
||||
output += pathEl.Render()
|
||||
}
|
||||
output += fmt.Sprintf(
|
||||
`<ellipse class="sketch-overlay" transform="translate(%d %d)" rx="%d" ry="%d" />`,
|
||||
shape.Pos.X+shape.Width/2, shape.Pos.Y+shape.Height/2, shape.Width/2, shape.Height/2,
|
||||
)
|
||||
soElement := d2themes.NewThemableElement("ellipse")
|
||||
soElement.SetTranslate(float64(shape.Pos.X+shape.Width/2), float64(shape.Pos.Y+shape.Height/2))
|
||||
soElement.Rx = float64(shape.Width / 2)
|
||||
soElement.Ry = float64(shape.Height / 2)
|
||||
renderedSO, err := d2themes.NewThemableSketchOverlay(
|
||||
soElement,
|
||||
shape.Fill,
|
||||
).Render()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
output += renderedSO
|
||||
|
||||
return output, nil
|
||||
}
|
||||
|
||||
|
|
@ -203,26 +281,35 @@ func Paths(r *Runner, shape d2target.Shape, paths []string) (string, error) {
|
|||
output := ""
|
||||
for _, path := range paths {
|
||||
js := fmt.Sprintf(`node = rc.path("%s", {
|
||||
fill: "%s",
|
||||
stroke: "%s",
|
||||
fill: "#000",
|
||||
stroke: "#000",
|
||||
strokeWidth: %d,
|
||||
%s
|
||||
});`, path, shape.Fill, shape.Stroke, shape.StrokeWidth, baseRoughProps)
|
||||
});`, path, shape.StrokeWidth, baseRoughProps)
|
||||
sketchPaths, err := computeRoughPathData(r, js)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
pathEl := d2themes.NewThemableElement("path")
|
||||
pathEl.Fill, pathEl.Stroke = d2themes.ShapeTheme(shape)
|
||||
pathEl.ClassName = "shape"
|
||||
pathEl.Style = shape.CSSStyle()
|
||||
for _, p := range sketchPaths {
|
||||
output += fmt.Sprintf(
|
||||
`<path class="shape" d="%s" style="%s" />`,
|
||||
p, shape.CSSStyle(),
|
||||
)
|
||||
pathEl.D = p
|
||||
output += pathEl.Render()
|
||||
}
|
||||
|
||||
soElement := d2themes.NewThemableElement("path")
|
||||
for _, p := range sketchPaths {
|
||||
output += fmt.Sprintf(
|
||||
`<path class="sketch-overlay" d="%s" />`,
|
||||
p,
|
||||
)
|
||||
soElement.D = p
|
||||
renderedSO, err := d2themes.NewThemableSketchOverlay(
|
||||
soElement,
|
||||
pathEl.Fill,
|
||||
).Render()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
output += renderedSO
|
||||
}
|
||||
}
|
||||
return output, nil
|
||||
|
|
@ -240,11 +327,16 @@ func Connection(r *Runner, connection d2target.Connection, path, attrs string) (
|
|||
if connection.Animated {
|
||||
animatedClass = " animated-connection"
|
||||
}
|
||||
|
||||
pathEl := d2themes.NewThemableElement("path")
|
||||
pathEl.Fill = color.None
|
||||
pathEl.Stroke = connection.Stroke
|
||||
pathEl.ClassName = fmt.Sprintf("connection%s", animatedClass)
|
||||
pathEl.Style = connection.CSSStyle()
|
||||
pathEl.Attributes = attrs
|
||||
for _, p := range paths {
|
||||
output += fmt.Sprintf(
|
||||
`<path class="connection%s" fill="none" d="%s" style="%s" %s/>`,
|
||||
animatedClass, p, connection.CSSStyle(), attrs,
|
||||
)
|
||||
pathEl.D = p
|
||||
output += pathEl.Render()
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
|
|
@ -253,20 +345,23 @@ func Connection(r *Runner, connection d2target.Connection, path, attrs string) (
|
|||
func Table(r *Runner, shape d2target.Shape) (string, error) {
|
||||
output := ""
|
||||
js := fmt.Sprintf(`node = rc.rectangle(0, 0, %d, %d, {
|
||||
fill: "%s",
|
||||
stroke: "%s",
|
||||
fill: "#000",
|
||||
stroke: "#000",
|
||||
strokeWidth: %d,
|
||||
%s
|
||||
});`, shape.Width, shape.Height, shape.Fill, shape.Stroke, shape.StrokeWidth, baseRoughProps)
|
||||
});`, shape.Width, shape.Height, shape.StrokeWidth, baseRoughProps)
|
||||
paths, err := computeRoughPathData(r, js)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
pathEl := d2themes.NewThemableElement("path")
|
||||
pathEl.SetTranslate(float64(shape.Pos.X), float64(shape.Pos.Y))
|
||||
pathEl.Fill, pathEl.Stroke = d2themes.ShapeTheme(shape)
|
||||
pathEl.ClassName = "shape"
|
||||
pathEl.Style = shape.CSSStyle()
|
||||
for _, p := range paths {
|
||||
output += fmt.Sprintf(
|
||||
`<path class="shape" transform="translate(%d %d)" d="%s" style="%s" />`,
|
||||
shape.Pos.X, shape.Pos.Y, p, shape.CSSStyle(),
|
||||
)
|
||||
pathEl.D = p
|
||||
output += pathEl.Render()
|
||||
}
|
||||
|
||||
box := geo.NewBox(
|
||||
|
|
@ -278,18 +373,20 @@ func Table(r *Runner, shape d2target.Shape) (string, error) {
|
|||
headerBox := geo.NewBox(box.TopLeft, box.Width, rowHeight)
|
||||
|
||||
js = fmt.Sprintf(`node = rc.rectangle(0, 0, %d, %f, {
|
||||
fill: "%s",
|
||||
fill: "#000",
|
||||
%s
|
||||
});`, shape.Width, rowHeight, shape.Fill, baseRoughProps)
|
||||
});`, shape.Width, rowHeight, baseRoughProps)
|
||||
paths, err = computeRoughPathData(r, js)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
pathEl = d2themes.NewThemableElement("path")
|
||||
pathEl.SetTranslate(float64(shape.Pos.X), float64(shape.Pos.Y))
|
||||
pathEl.Fill = shape.Fill
|
||||
pathEl.ClassName = "class_header"
|
||||
for _, p := range paths {
|
||||
output += fmt.Sprintf(
|
||||
`<path class="class_header" transform="translate(%d %d)" d="%s" style="fill:%s" />`,
|
||||
shape.Pos.X, shape.Pos.Y, p, shape.Fill,
|
||||
)
|
||||
pathEl.D = p
|
||||
output += pathEl.Render()
|
||||
}
|
||||
|
||||
if shape.Label != "" {
|
||||
|
|
@ -300,17 +397,16 @@ func Table(r *Runner, shape d2target.Shape) (string, error) {
|
|||
float64(shape.LabelHeight),
|
||||
)
|
||||
|
||||
output += fmt.Sprintf(`<text class="%s" x="%f" y="%f" style="%s">%s</text>`,
|
||||
"text",
|
||||
tl.X,
|
||||
tl.Y+float64(shape.LabelHeight)*3/4,
|
||||
fmt.Sprintf("text-anchor:%s;font-size:%vpx;fill:%s",
|
||||
"start",
|
||||
4+shape.FontSize,
|
||||
shape.Stroke,
|
||||
),
|
||||
svg.EscapeText(shape.Label),
|
||||
textEl := d2themes.NewThemableElement("text")
|
||||
textEl.X = tl.X
|
||||
textEl.Y = tl.Y + float64(shape.LabelHeight)*3/4
|
||||
textEl.Fill = shape.GetFontColor()
|
||||
textEl.ClassName = "text"
|
||||
textEl.Style = fmt.Sprintf("text-anchor:%s;font-size:%vpx",
|
||||
"start", 4+shape.FontSize,
|
||||
)
|
||||
textEl.Content = svg.EscapeText(shape.Label)
|
||||
output += textEl.Render()
|
||||
}
|
||||
|
||||
var longestNameWidth int
|
||||
|
|
@ -334,26 +430,26 @@ func Table(r *Runner, shape d2target.Shape) (string, error) {
|
|||
float64(shape.FontSize),
|
||||
)
|
||||
|
||||
output += strings.Join([]string{
|
||||
fmt.Sprintf(`<text class="text" x="%f" y="%f" style="%s">%s</text>`,
|
||||
nameTL.X,
|
||||
nameTL.Y+float64(shape.FontSize)*3/4,
|
||||
fmt.Sprintf("text-anchor:%s;font-size:%vpx;fill:%s", "start", float64(shape.FontSize), shape.PrimaryAccentColor),
|
||||
svg.EscapeText(f.Name.Label),
|
||||
),
|
||||
fmt.Sprintf(`<text class="text" x="%f" y="%f" style="%s">%s</text>`,
|
||||
nameTL.X+float64(longestNameWidth)+2*d2target.NamePadding,
|
||||
nameTL.Y+float64(shape.FontSize)*3/4,
|
||||
fmt.Sprintf("text-anchor:%s;font-size:%vpx;fill:%s", "start", float64(shape.FontSize), shape.NeutralAccentColor),
|
||||
svg.EscapeText(f.Type.Label),
|
||||
),
|
||||
fmt.Sprintf(`<text class="text" x="%f" y="%f" style="%s">%s</text>`,
|
||||
constraintTR.X,
|
||||
constraintTR.Y+float64(shape.FontSize)*3/4,
|
||||
fmt.Sprintf("text-anchor:%s;font-size:%vpx;fill:%s;letter-spacing:2px;", "end", float64(shape.FontSize), shape.SecondaryAccentColor),
|
||||
f.ConstraintAbbr(),
|
||||
),
|
||||
}, "\n")
|
||||
textEl := d2themes.NewThemableElement("text")
|
||||
textEl.X = nameTL.X
|
||||
textEl.Y = nameTL.Y + float64(shape.FontSize)*3/4
|
||||
textEl.Fill = shape.PrimaryAccentColor
|
||||
textEl.ClassName = "text"
|
||||
textEl.Style = fmt.Sprintf("text-anchor:%s;font-size:%vpx", "start", float64(shape.FontSize))
|
||||
textEl.Content = svg.EscapeText(f.Name.Label)
|
||||
output += textEl.Render()
|
||||
|
||||
textEl.X = nameTL.X + float64(longestNameWidth) + 2*d2target.NamePadding
|
||||
textEl.Fill = shape.NeutralAccentColor
|
||||
textEl.Content = svg.EscapeText(f.Type.Label)
|
||||
output += textEl.Render()
|
||||
|
||||
textEl.X = constraintTR.X
|
||||
textEl.Y = constraintTR.Y + float64(shape.FontSize)*3/4
|
||||
textEl.Fill = shape.SecondaryAccentColor
|
||||
textEl.Style = fmt.Sprintf("text-anchor:%s;font-size:%vpx;letter-spacing:2px", "end", float64(shape.FontSize))
|
||||
textEl.Content = f.ConstraintAbbr()
|
||||
output += textEl.Render()
|
||||
|
||||
rowBox.TopLeft.Y += rowHeight
|
||||
|
||||
|
|
@ -364,37 +460,47 @@ func Table(r *Runner, shape d2target.Shape) (string, error) {
|
|||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
pathEl := d2themes.NewThemableElement("path")
|
||||
pathEl.Fill = shape.Fill
|
||||
for _, p := range paths {
|
||||
output += fmt.Sprintf(
|
||||
`<path d="%s" style="fill:%s" />`,
|
||||
p, shape.Fill,
|
||||
)
|
||||
pathEl.D = p
|
||||
output += pathEl.Render()
|
||||
}
|
||||
}
|
||||
output += fmt.Sprintf(
|
||||
`<rect class="sketch-overlay" transform="translate(%d %d)" width="%d" height="%d" />`,
|
||||
shape.Pos.X, shape.Pos.Y, shape.Width, shape.Height,
|
||||
)
|
||||
|
||||
sketchOEl := d2themes.NewThemableElement("rect")
|
||||
sketchOEl.SetTranslate(float64(shape.Pos.X), float64(shape.Pos.Y))
|
||||
sketchOEl.Width = float64(shape.Width)
|
||||
sketchOEl.Height = float64(shape.Height)
|
||||
renderedSO, err := d2themes.NewThemableSketchOverlay(sketchOEl, pathEl.Fill).Render()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
output += renderedSO
|
||||
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func Class(r *Runner, shape d2target.Shape) (string, error) {
|
||||
output := ""
|
||||
js := fmt.Sprintf(`node = rc.rectangle(0, 0, %d, %d, {
|
||||
fill: "%s",
|
||||
stroke: "%s",
|
||||
fill: "#000",
|
||||
stroke: "#000",
|
||||
strokeWidth: %d,
|
||||
%s
|
||||
});`, shape.Width, shape.Height, shape.Fill, shape.Stroke, shape.StrokeWidth, baseRoughProps)
|
||||
});`, shape.Width, shape.Height, shape.StrokeWidth, baseRoughProps)
|
||||
paths, err := computeRoughPathData(r, js)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
pathEl := d2themes.NewThemableElement("path")
|
||||
pathEl.SetTranslate(float64(shape.Pos.X), float64(shape.Pos.Y))
|
||||
pathEl.Fill, pathEl.Stroke = d2themes.ShapeTheme(shape)
|
||||
pathEl.ClassName = "shape"
|
||||
pathEl.Style = shape.CSSStyle()
|
||||
for _, p := range paths {
|
||||
output += fmt.Sprintf(
|
||||
`<path class="shape" transform="translate(%d %d)" d="%s" style="%s" />`,
|
||||
shape.Pos.X, shape.Pos.Y, p, shape.CSSStyle(),
|
||||
)
|
||||
pathEl.D = p
|
||||
output += pathEl.Render()
|
||||
}
|
||||
|
||||
box := geo.NewBox(
|
||||
|
|
@ -407,24 +513,31 @@ func Class(r *Runner, shape d2target.Shape) (string, error) {
|
|||
headerBox := geo.NewBox(box.TopLeft, box.Width, 2*rowHeight)
|
||||
|
||||
js = fmt.Sprintf(`node = rc.rectangle(0, 0, %d, %f, {
|
||||
fill: "%s",
|
||||
fill: "#000",
|
||||
%s
|
||||
});`, shape.Width, headerBox.Height, shape.Fill, baseRoughProps)
|
||||
});`, shape.Width, headerBox.Height, baseRoughProps)
|
||||
paths, err = computeRoughPathData(r, js)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
pathEl = d2themes.NewThemableElement("path")
|
||||
pathEl.SetTranslate(float64(shape.Pos.X), float64(shape.Pos.Y))
|
||||
pathEl.Fill = shape.Fill
|
||||
pathEl.ClassName = "class_header"
|
||||
for _, p := range paths {
|
||||
output += fmt.Sprintf(
|
||||
`<path class="class_header" transform="translate(%d %d)" d="%s" style="fill:%s" />`,
|
||||
shape.Pos.X, shape.Pos.Y, p, shape.Fill,
|
||||
)
|
||||
pathEl.D = p
|
||||
output += pathEl.Render()
|
||||
}
|
||||
|
||||
output += fmt.Sprintf(
|
||||
`<rect class="sketch-overlay" transform="translate(%d %d)" width="%d" height="%f" />`,
|
||||
shape.Pos.X, shape.Pos.Y, shape.Width, headerBox.Height,
|
||||
)
|
||||
sketchOEl := d2themes.NewThemableElement("rect")
|
||||
sketchOEl.SetTranslate(float64(shape.Pos.X), float64(shape.Pos.Y))
|
||||
sketchOEl.Width = float64(shape.Width)
|
||||
sketchOEl.Height = headerBox.Height
|
||||
renderedSO, err := d2themes.NewThemableSketchOverlay(sketchOEl, pathEl.Fill).Render()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
output += renderedSO
|
||||
|
||||
if shape.Label != "" {
|
||||
tl := label.InsideMiddleCenter.GetPointOnBox(
|
||||
|
|
@ -434,17 +547,17 @@ func Class(r *Runner, shape d2target.Shape) (string, error) {
|
|||
float64(shape.LabelHeight),
|
||||
)
|
||||
|
||||
output += fmt.Sprintf(`<text class="%s" x="%f" y="%f" style="%s">%s</text>`,
|
||||
"text-mono",
|
||||
tl.X+float64(shape.LabelWidth)/2,
|
||||
tl.Y+float64(shape.LabelHeight)*3/4,
|
||||
fmt.Sprintf("text-anchor:%s;font-size:%vpx;fill:%s",
|
||||
"middle",
|
||||
4+shape.FontSize,
|
||||
shape.Stroke,
|
||||
),
|
||||
svg.EscapeText(shape.Label),
|
||||
textEl := d2themes.NewThemableElement("text")
|
||||
textEl.X = tl.X + float64(shape.LabelWidth)/2
|
||||
textEl.Y = tl.Y + float64(shape.LabelHeight)*3/4
|
||||
textEl.Fill = shape.GetFontColor()
|
||||
textEl.ClassName = "text-mono"
|
||||
textEl.Style = fmt.Sprintf("text-anchor:%s;font-size:%vpx",
|
||||
"middle",
|
||||
4+shape.FontSize,
|
||||
)
|
||||
textEl.Content = svg.EscapeText(shape.Label)
|
||||
output += textEl.Render()
|
||||
}
|
||||
|
||||
rowBox := geo.NewBox(box.TopLeft.Copy(), box.Width, rowHeight)
|
||||
|
|
@ -461,11 +574,12 @@ func Class(r *Runner, shape d2target.Shape) (string, error) {
|
|||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
pathEl = d2themes.NewThemableElement("path")
|
||||
pathEl.Fill = shape.Fill
|
||||
pathEl.ClassName = "class_header"
|
||||
for _, p := range paths {
|
||||
output += fmt.Sprintf(
|
||||
`<path class="class_header" d="%s" style="fill:%s" />`,
|
||||
p, shape.Fill,
|
||||
)
|
||||
pathEl.D = p
|
||||
output += pathEl.Render()
|
||||
}
|
||||
|
||||
for _, m := range shape.Methods {
|
||||
|
|
@ -491,28 +605,27 @@ func classRow(shape d2target.Shape, box *geo.Box, prefix, nameText, typeText str
|
|||
fontSize,
|
||||
)
|
||||
|
||||
output += strings.Join([]string{
|
||||
fmt.Sprintf(`<text class="text-mono" x="%f" y="%f" style="%s">%s</text>`,
|
||||
prefixTL.X,
|
||||
prefixTL.Y+fontSize*3/4,
|
||||
fmt.Sprintf("text-anchor:%s;font-size:%vpx;fill:%s", "start", fontSize, shape.PrimaryAccentColor),
|
||||
prefix,
|
||||
),
|
||||
textEl := d2themes.NewThemableElement("text")
|
||||
textEl.X = prefixTL.X
|
||||
textEl.Y = prefixTL.Y + fontSize*3/4
|
||||
textEl.Fill = shape.PrimaryAccentColor
|
||||
textEl.ClassName = "text-mono"
|
||||
textEl.Style = fmt.Sprintf("text-anchor:%s;font-size:%vpx", "start", fontSize)
|
||||
textEl.Content = prefix
|
||||
output += textEl.Render()
|
||||
|
||||
fmt.Sprintf(`<text class="text-mono" x="%f" y="%f" style="%s">%s</text>`,
|
||||
prefixTL.X+d2target.PrefixWidth,
|
||||
prefixTL.Y+fontSize*3/4,
|
||||
fmt.Sprintf("text-anchor:%s;font-size:%vpx;fill:%s", "start", fontSize, shape.Fill),
|
||||
svg.EscapeText(nameText),
|
||||
),
|
||||
textEl.X = prefixTL.X + d2target.PrefixWidth
|
||||
textEl.Fill = shape.Fill
|
||||
textEl.Content = svg.EscapeText(nameText)
|
||||
output += textEl.Render()
|
||||
|
||||
textEl.X = typeTR.X
|
||||
textEl.Y = typeTR.Y + fontSize*3/4
|
||||
textEl.Fill = shape.SecondaryAccentColor
|
||||
textEl.Style = fmt.Sprintf("text-anchor:%s;font-size:%vpx", "end", fontSize)
|
||||
textEl.Content = svg.EscapeText(typeText)
|
||||
output += textEl.Render()
|
||||
|
||||
fmt.Sprintf(`<text class="text-mono" x="%f" y="%f" style="%s">%s</text>`,
|
||||
typeTR.X,
|
||||
typeTR.Y+fontSize*3/4,
|
||||
fmt.Sprintf("text-anchor:%s;font-size:%vpx;fill:%s;", "end", fontSize, shape.SecondaryAccentColor),
|
||||
svg.EscapeText(typeText),
|
||||
),
|
||||
}, "\n")
|
||||
return output
|
||||
}
|
||||
|
||||
|
|
@ -551,12 +664,6 @@ type roughPath struct {
|
|||
|
||||
func (rp roughPath) StyleCSS() string {
|
||||
style := ""
|
||||
if rp.Style.Fill != "" {
|
||||
style += fmt.Sprintf("fill:%s;", rp.Style.Fill)
|
||||
}
|
||||
if rp.Style.Stroke != "" {
|
||||
style += fmt.Sprintf("stroke:%s;", rp.Style.Stroke)
|
||||
}
|
||||
if rp.Style.StrokeWidth != "" {
|
||||
style += fmt.Sprintf("stroke-width:%s;", rp.Style.StrokeWidth)
|
||||
}
|
||||
|
|
@ -617,10 +724,11 @@ func ArrowheadJS(r *Runner, arrowhead d2target.Arrowhead, stroke string, strokeW
|
|||
)
|
||||
case d2target.DiamondArrowhead:
|
||||
arrowJS = fmt.Sprintf(
|
||||
`node = rc.polygon(%s, { strokeWidth: %d, stroke: "%s", fill: "white", fillStyle: "solid", seed: 1 })`,
|
||||
`node = rc.polygon(%s, { strokeWidth: %d, stroke: "%s", fill: "%s", fillStyle: "solid", seed: 1 })`,
|
||||
`[[-20, 0], [-10, 5], [0, 0], [-10, -5], [-20, 0]]`,
|
||||
strokeWidth,
|
||||
stroke,
|
||||
BG_COLOR,
|
||||
)
|
||||
case d2target.FilledDiamondArrowhead:
|
||||
arrowJS = fmt.Sprintf(
|
||||
|
|
@ -648,9 +756,10 @@ func ArrowheadJS(r *Runner, arrowhead d2target.Arrowhead, stroke string, strokeW
|
|||
stroke,
|
||||
)
|
||||
extraJS = fmt.Sprintf(
|
||||
`node = rc.circle(-20, 0, 8, { strokeWidth: %d, stroke: "%s", fill: "white", fillStyle: "solid", fillWeight: 1, seed: 4 })`,
|
||||
`node = rc.circle(-20, 0, 8, { strokeWidth: %d, stroke: "%s", fill: "%s", fillStyle: "solid", fillWeight: 1, seed: 4 })`,
|
||||
strokeWidth,
|
||||
stroke,
|
||||
BG_COLOR,
|
||||
)
|
||||
case d2target.CfOneRequired:
|
||||
arrowJS = fmt.Sprintf(
|
||||
|
|
@ -669,9 +778,10 @@ func ArrowheadJS(r *Runner, arrowhead d2target.Arrowhead, stroke string, strokeW
|
|||
stroke,
|
||||
)
|
||||
extraJS = fmt.Sprintf(
|
||||
`node = rc.circle(-20, 0, 8, { strokeWidth: %d, stroke: "%s", fill: "white", fillStyle: "solid", fillWeight: 1, seed: 5 })`,
|
||||
`node = rc.circle(-20, 0, 8, { strokeWidth: %d, stroke: "%s", fill: "%s", fillStyle: "solid", fillWeight: 1, seed: 5 })`,
|
||||
strokeWidth,
|
||||
stroke,
|
||||
BG_COLOR,
|
||||
)
|
||||
}
|
||||
return
|
||||
|
|
@ -706,13 +816,15 @@ func Arrowheads(r *Runner, connection d2target.Connection, srcAdj, dstAdj *geo.P
|
|||
roughPaths = append(roughPaths, extraPaths...)
|
||||
}
|
||||
|
||||
pathEl := d2themes.NewThemableElement("path")
|
||||
pathEl.ClassName = "connection"
|
||||
pathEl.Attributes = transform
|
||||
for _, rp := range roughPaths {
|
||||
pathStr := fmt.Sprintf(`<path class="connection" d="%s" style="%s" %s/>`,
|
||||
rp.Attrs.D,
|
||||
rp.StyleCSS(),
|
||||
transform,
|
||||
)
|
||||
arrowPaths = append(arrowPaths, pathStr)
|
||||
pathEl.D = rp.Attrs.D
|
||||
pathEl.Fill = rp.Style.Fill
|
||||
pathEl.Stroke = rp.Style.Stroke
|
||||
pathEl.Style = rp.StyleCSS()
|
||||
arrowPaths = append(arrowPaths, pathEl.Render())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -743,13 +855,15 @@ func Arrowheads(r *Runner, connection d2target.Connection, srcAdj, dstAdj *geo.P
|
|||
roughPaths = append(roughPaths, extraPaths...)
|
||||
}
|
||||
|
||||
pathEl := d2themes.NewThemableElement("path")
|
||||
pathEl.ClassName = "connection"
|
||||
pathEl.Attributes = transform
|
||||
for _, rp := range roughPaths {
|
||||
pathStr := fmt.Sprintf(`<path class="connection" d="%s" style="%s" %s/>`,
|
||||
rp.Attrs.D,
|
||||
rp.StyleCSS(),
|
||||
transform,
|
||||
)
|
||||
arrowPaths = append(arrowPaths, pathStr)
|
||||
pathEl.D = rp.Attrs.D
|
||||
pathEl.Fill = rp.Style.Fill
|
||||
pathEl.Stroke = rp.Style.Stroke
|
||||
pathEl.Style = rp.StyleCSS()
|
||||
arrowPaths = append(arrowPaths, pathEl.Render())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -49,6 +49,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: "" {
|
||||
|
|
@ -345,6 +471,538 @@ users: {
|
|||
last_login: datetime
|
||||
style.opacity: 0.4
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "overlay",
|
||||
script: `bright: {
|
||||
style.stroke: "#000"
|
||||
style.font-color: "#000"
|
||||
style.fill: "#fff"
|
||||
}
|
||||
normal: {
|
||||
style.stroke: "#000"
|
||||
style.font-color: "#000"
|
||||
style.fill: "#ccc"
|
||||
}
|
||||
dark: {
|
||||
style.stroke: "#000"
|
||||
style.font-color: "#fff"
|
||||
style.fill: "#555"
|
||||
}
|
||||
darker: {
|
||||
style.stroke: "#000"
|
||||
style.font-color: "#fff"
|
||||
style.fill: "#000"
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "basic dark",
|
||||
themeID: 200,
|
||||
script: `a -> b
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "child to child dark",
|
||||
themeID: 200,
|
||||
script: `winter.snow -> summer.sun
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "animated dark",
|
||||
themeID: 200,
|
||||
script: `winter.snow -> summer.sun -> trees -> winter.snow: { style.animated: true }
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "connection label dark",
|
||||
themeID: 200,
|
||||
script: `a -> b: hello
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "crows feet dark",
|
||||
themeID: 200,
|
||||
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 dark",
|
||||
themeID: 200,
|
||||
script: `timeline mixer: "" {
|
||||
explanation: |md
|
||||
## **Timeline mixer**
|
||||
- Inject ads, who-to-follow, onboarding
|
||||
- Conversation module
|
||||
- Cursoring,pagination
|
||||
- Tweat deduplication
|
||||
- Served data logging
|
||||
|
|
||||
}
|
||||
People discovery: "People discovery \nservice"
|
||||
admixer: Ad mixer {
|
||||
style.fill: "#c1a2f3"
|
||||
}
|
||||
|
||||
onboarding service: "Onboarding \nservice"
|
||||
timeline mixer -> People discovery
|
||||
timeline mixer -> onboarding service
|
||||
timeline mixer -> admixer
|
||||
container0: "" {
|
||||
graphql
|
||||
comment
|
||||
tlsapi
|
||||
}
|
||||
container0.graphql: GraphQL\nFederated Strato Column {
|
||||
shape: image
|
||||
icon: https://upload.wikimedia.org/wikipedia/commons/thumb/1/17/GraphQL_Logo.svg/1200px-GraphQL_Logo.svg.png
|
||||
}
|
||||
container0.comment: |md
|
||||
## Tweet/user content hydration, visibility filtering
|
||||
|
|
||||
container0.tlsapi: TLS-API (being deprecated)
|
||||
container0.graphql -> timeline mixer
|
||||
timeline mixer <- container0.tlsapi
|
||||
twitter fe: "Twitter Frontend " {
|
||||
icon: https://icons.terrastruct.com/social/013-twitter-1.svg
|
||||
shape: image
|
||||
}
|
||||
twitter fe -> container0.graphql: iPhone web
|
||||
twitter fe -> container0.tlsapi: HTTP Android
|
||||
web: Web {
|
||||
icon: https://icons.terrastruct.com/azure/Web%20Service%20Color/App%20Service%20Domains.svg
|
||||
shape: image
|
||||
}
|
||||
|
||||
Iphone: {
|
||||
icon: 'https://ss7.vzw.com/is/image/VerizonWireless/apple-iphone-12-64gb-purple-53017-mjn13ll-a?$device-lg$'
|
||||
shape: image
|
||||
}
|
||||
Android: {
|
||||
icon: https://cdn4.iconfinder.com/data/icons/smart-phones-technologies/512/android-phone.png
|
||||
shape: image
|
||||
}
|
||||
|
||||
web -> twitter fe
|
||||
timeline scorer: "Timeline\nScorer" {
|
||||
style.fill "#ffdef1"
|
||||
}
|
||||
home ranker: Home Ranker
|
||||
|
||||
timeline service: Timeline Service
|
||||
timeline mixer -> timeline scorer: Thrift RPC
|
||||
timeline mixer -> home ranker: {
|
||||
style.stroke-dash: 4
|
||||
style.stroke: "#000E3D"
|
||||
}
|
||||
timeline mixer -> timeline service
|
||||
home mixer: Home mixer {
|
||||
# style.fill "#c1a2f3"
|
||||
}
|
||||
container0.graphql -> home mixer: {
|
||||
style.stroke-dash: 4
|
||||
style.stroke: "#000E3D"
|
||||
}
|
||||
home mixer -> timeline scorer
|
||||
home mixer -> home ranker: {
|
||||
style.stroke-dash: 4
|
||||
style.stroke: "#000E3D"
|
||||
}
|
||||
home mixer -> timeline service
|
||||
manhattan 2: Manhattan
|
||||
gizmoduck: Gizmoduck
|
||||
socialgraph: Social graph
|
||||
tweetypie: Tweety Pie
|
||||
home mixer -> manhattan 2
|
||||
home mixer -> gizmoduck
|
||||
home mixer -> socialgraph
|
||||
home mixer -> tweetypie
|
||||
Iphone -> twitter fe
|
||||
Android -> twitter fe
|
||||
prediction service2: Prediction Service {
|
||||
shape: image
|
||||
icon: https://cdn-icons-png.flaticon.com/512/6461/6461819.png
|
||||
}
|
||||
home scorer: Home Scorer {
|
||||
style.fill "#ffdef1"
|
||||
}
|
||||
manhattan: Manhattan
|
||||
memcache: Memcache {
|
||||
icon: https://d1q6f0aelx0por.cloudfront.net/product-logos/de041504-0ddb-43f6-b89e-fe04403cca8d-memcached.png
|
||||
}
|
||||
|
||||
fetch: Fetch {
|
||||
style.multiple: true
|
||||
shape: step
|
||||
}
|
||||
feature: Feature {
|
||||
style.multiple: true
|
||||
shape: step
|
||||
}
|
||||
scoring: Scoring {
|
||||
style.multiple: true
|
||||
shape: step
|
||||
}
|
||||
fetch -> feature
|
||||
feature -> scoring
|
||||
|
||||
prediction service: Prediction Service {
|
||||
shape: image
|
||||
icon: https://cdn-icons-png.flaticon.com/512/6461/6461819.png
|
||||
}
|
||||
scoring -> prediction service
|
||||
fetch -> container2.crmixer
|
||||
|
||||
home scorer -> manhattan: ""
|
||||
|
||||
home scorer -> memcache: ""
|
||||
home scorer -> prediction service2
|
||||
home ranker -> home scorer
|
||||
home ranker -> container2.crmixer: Candidate Fetch
|
||||
container2: "" {
|
||||
style.stroke: "#000E3D"
|
||||
style.fill: "#ffffff"
|
||||
crmixer: CrMixer {
|
||||
style.fill: "#F7F8FE"
|
||||
}
|
||||
earlybird: EarlyBird
|
||||
utag: Utag
|
||||
space: Space
|
||||
communities: Communities
|
||||
}
|
||||
etc: ...etc
|
||||
|
||||
home scorer -> etc: Feature Hydration
|
||||
|
||||
feature -> manhattan
|
||||
feature -> memcache
|
||||
feature -> etc: Candidate sources
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "all_shapes dark",
|
||||
themeID: 200,
|
||||
script: `
|
||||
rectangle: {shape: "rectangle"}
|
||||
square: {shape: "square"}
|
||||
page: {shape: "page"}
|
||||
parallelogram: {shape: "parallelogram"}
|
||||
document: {shape: "document"}
|
||||
cylinder: {shape: "cylinder"}
|
||||
queue: {shape: "queue"}
|
||||
package: {shape: "package"}
|
||||
step: {shape: "step"}
|
||||
callout: {shape: "callout"}
|
||||
stored_data: {shape: "stored_data"}
|
||||
person: {shape: "person"}
|
||||
diamond: {shape: "diamond"}
|
||||
oval: {shape: "oval"}
|
||||
circle: {shape: "circle"}
|
||||
hexagon: {shape: "hexagon"}
|
||||
cloud: {shape: "cloud"}
|
||||
|
||||
rectangle -> square -> page
|
||||
parallelogram -> document -> cylinder
|
||||
queue -> package -> step
|
||||
callout -> stored_data -> person
|
||||
diamond -> oval -> circle
|
||||
hexagon -> cloud
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "sql_tables dark",
|
||||
themeID: 200,
|
||||
script: `users: {
|
||||
shape: sql_table
|
||||
id: int
|
||||
name: string
|
||||
email: string
|
||||
password: string
|
||||
last_login: datetime
|
||||
}
|
||||
|
||||
products: {
|
||||
shape: sql_table
|
||||
id: int
|
||||
price: decimal
|
||||
sku: string
|
||||
name: string
|
||||
}
|
||||
|
||||
orders: {
|
||||
shape: sql_table
|
||||
id: int
|
||||
user_id: int
|
||||
product_id: int
|
||||
}
|
||||
|
||||
shipments: {
|
||||
shape: sql_table
|
||||
id: int
|
||||
order_id: int
|
||||
tracking_number: string {constraint: primary_key}
|
||||
status: string
|
||||
}
|
||||
|
||||
users.id <-> orders.user_id
|
||||
products.id <-> orders.product_id
|
||||
shipments.order_id <-> orders.id`,
|
||||
},
|
||||
{
|
||||
name: "class dark",
|
||||
themeID: 200,
|
||||
script: `manager: BatchManager {
|
||||
shape: class
|
||||
-num: int
|
||||
-timeout: int
|
||||
-pid
|
||||
|
||||
+getStatus(): Enum
|
||||
+getJobs(): "Job[]"
|
||||
+setTimeout(seconds int)
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "arrowheads dark",
|
||||
themeID: 200,
|
||||
script: `
|
||||
a: ""
|
||||
b: ""
|
||||
a.1 -- b.1: none
|
||||
a.2 <-> b.2: arrow {
|
||||
source-arrowhead.shape: arrow
|
||||
target-arrowhead.shape: arrow
|
||||
}
|
||||
a.3 <-> b.3: triangle {
|
||||
source-arrowhead.shape: triangle
|
||||
target-arrowhead.shape: triangle
|
||||
}
|
||||
a.4 <-> b.4: diamond {
|
||||
source-arrowhead.shape: diamond
|
||||
target-arrowhead.shape: diamond
|
||||
}
|
||||
a.5 <-> b.5: diamond filled {
|
||||
source-arrowhead: {
|
||||
shape: diamond
|
||||
style.filled: true
|
||||
}
|
||||
target-arrowhead: {
|
||||
shape: diamond
|
||||
style.filled: true
|
||||
}
|
||||
}
|
||||
a.6 <-> b.6: cf-many {
|
||||
source-arrowhead.shape: cf-many
|
||||
target-arrowhead.shape: cf-many
|
||||
}
|
||||
a.7 <-> b.7: cf-many-required {
|
||||
source-arrowhead.shape: cf-many-required
|
||||
target-arrowhead.shape: cf-many-required
|
||||
}
|
||||
a.8 <-> b.8: cf-one {
|
||||
source-arrowhead.shape: cf-one
|
||||
target-arrowhead.shape: cf-one
|
||||
}
|
||||
a.9 <-> b.9: cf-one-required {
|
||||
source-arrowhead.shape: cf-one-required
|
||||
target-arrowhead.shape: cf-one-required
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "opacity dark",
|
||||
themeID: 200,
|
||||
script: `x.style.opacity: 0.4
|
||||
y: |md
|
||||
linux: because a PC is a terrible thing to waste
|
||||
| {
|
||||
style.opacity: 0.4
|
||||
}
|
||||
x -> a: {
|
||||
label: You don't have to know how the computer works,\njust how to work the computer.
|
||||
style.opacity: 0.4
|
||||
}
|
||||
users: {
|
||||
shape: sql_table
|
||||
last_login: datetime
|
||||
style.opacity: 0.4
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "root-fill",
|
||||
script: `style.fill: honeydew
|
||||
style.stroke: LightSteelBlue
|
||||
style.double-border: true
|
||||
|
||||
title: Flow-I (Warehousing, Installation) {
|
||||
near: top-center
|
||||
shape: text
|
||||
style: {
|
||||
font-size: 24
|
||||
bold: false
|
||||
underline: false
|
||||
}
|
||||
}
|
||||
OEM Factory
|
||||
OEM Factory -> OEM Warehouse
|
||||
OEM Factory -> Distributor Warehouse
|
||||
OEM Factory -> company Warehouse
|
||||
|
||||
company Warehouse.Master -> company Warehouse.Regional-1
|
||||
company Warehouse.Master -> company Warehouse.Regional-2
|
||||
company Warehouse.Master -> company Warehouse.Regional-N
|
||||
company Warehouse.Regional-1 -> company Warehouse.Regional-2
|
||||
company Warehouse.Regional-2 -> company Warehouse.Regional-N
|
||||
company Warehouse.Regional-N -> company Warehouse.Regional-1
|
||||
|
||||
company Warehouse.explanation: |md
|
||||
### company Warehouse
|
||||
- Asset Tagging
|
||||
- Inventory
|
||||
- Staging
|
||||
- Dispatch to Site
|
||||
|
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "double-border",
|
||||
script: `a: {
|
||||
style.double-border: true
|
||||
b
|
||||
}
|
||||
c: {
|
||||
shape: oval
|
||||
style.double-border: true
|
||||
d
|
||||
}
|
||||
normal: {
|
||||
nested normal
|
||||
}
|
||||
something
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
|
@ -352,9 +1010,10 @@ users: {
|
|||
}
|
||||
|
||||
type testCase struct {
|
||||
name string
|
||||
script string
|
||||
skip bool
|
||||
name string
|
||||
themeID int64
|
||||
script string
|
||||
skip bool
|
||||
}
|
||||
|
||||
func runa(t *testing.T, tcs []testCase) {
|
||||
|
|
@ -383,7 +1042,6 @@ func run(t *testing.T, tc testCase) {
|
|||
|
||||
diagram, _, err := d2lib.Compile(ctx, tc.script, &d2lib.CompileOptions{
|
||||
Ruler: ruler,
|
||||
ThemeID: 0,
|
||||
Layout: d2dagrelayout.DefaultLayout,
|
||||
FontFamily: go2.Pointer(d2fonts.HandDrawn),
|
||||
})
|
||||
|
|
@ -395,8 +1053,9 @@ func run(t *testing.T, tc testCase) {
|
|||
pathGotSVG := filepath.Join(dataPath, "sketch.got.svg")
|
||||
|
||||
svgBytes, err := d2svg.Render(diagram, &d2svg.RenderOpts{
|
||||
Pad: d2svg.DEFAULT_PADDING,
|
||||
Sketch: true,
|
||||
Pad: d2svg.DEFAULT_PADDING,
|
||||
Sketch: true,
|
||||
ThemeID: tc.themeID,
|
||||
})
|
||||
assert.Success(t, err)
|
||||
err = os.MkdirAll(dataPath, 0755)
|
||||
|
|
@ -409,6 +1068,6 @@ func run(t *testing.T, tc testCase) {
|
|||
err = xml.Unmarshal(svgBytes, &xmlParsed)
|
||||
assert.Success(t, err)
|
||||
|
||||
err = diff.Testdata(filepath.Join(dataPath, "sketch"), ".svg", svgBytes)
|
||||
assert.Success(t, err)
|
||||
// We want the visual diffs to compare, but there's floating point precision differences between CI and user machines, so don't compare raw strings
|
||||
diff.Testdata(filepath.Join(dataPath, "sketch"), ".svg", svgBytes)
|
||||
}
|
||||
|
|
|
|||
3
d2renderers/d2sketch/streaks.txt
Normal file
|
Before Width: | Height: | Size: 267 KiB After Width: | Height: | Size: 298 KiB |
29
d2renderers/d2sketch/testdata/all_shapes_dark/sketch.exp.svg
vendored
Normal file
|
After Width: | Height: | Size: 288 KiB |
|
Before Width: | Height: | Size: 253 KiB After Width: | Height: | Size: 284 KiB |
42
d2renderers/d2sketch/testdata/animated_dark/sketch.exp.svg
vendored
Normal file
|
After Width: | Height: | Size: 274 KiB |
|
Before Width: | Height: | Size: 303 KiB After Width: | Height: | Size: 334 KiB |
44
d2renderers/d2sketch/testdata/arrowheads_dark/sketch.exp.svg
vendored
Normal file
|
After Width: | Height: | Size: 325 KiB |
|
Before Width: | Height: | Size: 196 KiB After Width: | Height: | Size: 227 KiB |
29
d2renderers/d2sketch/testdata/basic_dark/sketch.exp.svg
vendored
Normal file
|
After Width: | Height: | Size: 218 KiB |
|
Before Width: | Height: | Size: 248 KiB After Width: | Height: | Size: 279 KiB |
36
d2renderers/d2sketch/testdata/child_to_child_dark/sketch.exp.svg
vendored
Normal file
|
After Width: | Height: | Size: 270 KiB |
|
Before Width: | Height: | Size: 196 KiB After Width: | Height: | Size: 226 KiB |
29
d2renderers/d2sketch/testdata/class_dark/sketch.exp.svg
vendored
Normal file
|
After Width: | Height: | Size: 217 KiB |
136
d2renderers/d2sketch/testdata/connection_label/board.exp.json
generated
vendored
|
|
@ -1,136 +0,0 @@
|
|||
{
|
||||
"name": "",
|
||||
"fontFamily": "HandDrawn",
|
||||
"shapes": [
|
||||
{
|
||||
"id": "a",
|
||||
"type": "",
|
||||
"pos": {
|
||||
"x": 1,
|
||||
"y": 0
|
||||
},
|
||||
"width": 114,
|
||||
"height": 126,
|
||||
"opacity": 1,
|
||||
"strokeDash": 0,
|
||||
"strokeWidth": 2,
|
||||
"borderRadius": 0,
|
||||
"fill": "#F7F8FE",
|
||||
"stroke": "#0D32B2",
|
||||
"shadow": false,
|
||||
"3d": false,
|
||||
"multiple": false,
|
||||
"tooltip": "",
|
||||
"link": "",
|
||||
"icon": null,
|
||||
"iconPosition": "",
|
||||
"blend": false,
|
||||
"fields": null,
|
||||
"methods": null,
|
||||
"columns": null,
|
||||
"label": "a",
|
||||
"fontSize": 16,
|
||||
"fontFamily": "DEFAULT",
|
||||
"language": "",
|
||||
"color": "#0A0F25",
|
||||
"italic": false,
|
||||
"bold": true,
|
||||
"underline": false,
|
||||
"labelWidth": 14,
|
||||
"labelHeight": 26,
|
||||
"labelPosition": "INSIDE_MIDDLE_CENTER",
|
||||
"zIndex": 0,
|
||||
"level": 1
|
||||
},
|
||||
{
|
||||
"id": "b",
|
||||
"type": "",
|
||||
"pos": {
|
||||
"x": 0,
|
||||
"y": 226
|
||||
},
|
||||
"width": 115,
|
||||
"height": 126,
|
||||
"opacity": 1,
|
||||
"strokeDash": 0,
|
||||
"strokeWidth": 2,
|
||||
"borderRadius": 0,
|
||||
"fill": "#F7F8FE",
|
||||
"stroke": "#0D32B2",
|
||||
"shadow": false,
|
||||
"3d": false,
|
||||
"multiple": false,
|
||||
"tooltip": "",
|
||||
"link": "",
|
||||
"icon": null,
|
||||
"iconPosition": "",
|
||||
"blend": false,
|
||||
"fields": null,
|
||||
"methods": null,
|
||||
"columns": null,
|
||||
"label": "b",
|
||||
"fontSize": 16,
|
||||
"fontFamily": "DEFAULT",
|
||||
"language": "",
|
||||
"color": "#0A0F25",
|
||||
"italic": false,
|
||||
"bold": true,
|
||||
"underline": false,
|
||||
"labelWidth": 15,
|
||||
"labelHeight": 26,
|
||||
"labelPosition": "INSIDE_MIDDLE_CENTER",
|
||||
"zIndex": 0,
|
||||
"level": 1
|
||||
}
|
||||
],
|
||||
"connections": [
|
||||
{
|
||||
"id": "(a -> b)[0]",
|
||||
"src": "a",
|
||||
"srcArrow": "none",
|
||||
"srcLabel": "",
|
||||
"dst": "b",
|
||||
"dstArrow": "triangle",
|
||||
"dstLabel": "",
|
||||
"opacity": 1,
|
||||
"strokeDash": 0,
|
||||
"strokeWidth": 2,
|
||||
"stroke": "#0D32B2",
|
||||
"label": "hello",
|
||||
"fontSize": 16,
|
||||
"fontFamily": "DEFAULT",
|
||||
"language": "",
|
||||
"color": "#676C7E",
|
||||
"italic": true,
|
||||
"bold": false,
|
||||
"underline": false,
|
||||
"labelWidth": 31,
|
||||
"labelHeight": 23,
|
||||
"labelPosition": "INSIDE_MIDDLE_CENTER",
|
||||
"labelPercentage": 0,
|
||||
"route": [
|
||||
{
|
||||
"x": 57.5,
|
||||
"y": 126
|
||||
},
|
||||
{
|
||||
"x": 57.5,
|
||||
"y": 166
|
||||
},
|
||||
{
|
||||
"x": 57.5,
|
||||
"y": 186
|
||||
},
|
||||
{
|
||||
"x": 57.5,
|
||||
"y": 226
|
||||
}
|
||||
],
|
||||
"isCurve": true,
|
||||
"animated": false,
|
||||
"tooltip": "",
|
||||
"icon": null,
|
||||
"zIndex": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
|
Before Width: | Height: | Size: 246 KiB After Width: | Height: | Size: 276 KiB |
36
d2renderers/d2sketch/testdata/connection_label_dark/sketch.exp.svg
vendored
Normal file
|
After Width: | Height: | Size: 267 KiB |
31
d2renderers/d2sketch/testdata/crows_feet/sketch.exp.svg
vendored
Normal file
|
After Width: | Height: | Size: 332 KiB |
29
d2renderers/d2sketch/testdata/crows_feet_dark/sketch.exp.svg
vendored
Normal file
|
After Width: | Height: | Size: 323 KiB |
38
d2renderers/d2sketch/testdata/double-border/sketch.exp.svg
vendored
Normal file
|
After Width: | Height: | Size: 289 KiB |
124
d2renderers/d2sketch/testdata/opacity/sketch.exp.svg
vendored
|
Before Width: | Height: | Size: 309 KiB After Width: | Height: | Size: 340 KiB |
786
d2renderers/d2sketch/testdata/opacity_dark/sketch.exp.svg
vendored
Normal file
|
After Width: | Height: | Size: 330 KiB |
47
d2renderers/d2sketch/testdata/overlay/sketch.exp.svg
vendored
Normal file
|
After Width: | Height: | Size: 228 KiB |
787
d2renderers/d2sketch/testdata/root-fill/sketch.exp.svg
vendored
Normal file
|
After Width: | Height: | Size: 306 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 114 KiB |
29
d2renderers/d2sketch/testdata/sql_tables_dark/sketch.exp.svg
vendored
Normal file
|
After Width: | Height: | Size: 105 KiB |
144
d2renderers/d2sketch/testdata/twitter/sketch.exp.svg
vendored
|
Before Width: | Height: | Size: 387 KiB After Width: | Height: | Size: 418 KiB |
809
d2renderers/d2sketch/testdata/twitter_dark/sketch.exp.svg
vendored
Normal file
|
After Width: | Height: | Size: 418 KiB |
|
|
@ -14,6 +14,8 @@ import (
|
|||
"oss.terrastruct.com/d2/d2renderers/d2fonts"
|
||||
"oss.terrastruct.com/d2/d2renderers/d2svg"
|
||||
"oss.terrastruct.com/d2/d2target"
|
||||
"oss.terrastruct.com/d2/d2themes"
|
||||
"oss.terrastruct.com/d2/lib/color"
|
||||
"oss.terrastruct.com/d2/lib/textmeasure"
|
||||
"oss.terrastruct.com/util-go/go2"
|
||||
)
|
||||
|
|
@ -48,8 +50,8 @@ const (
|
|||
)
|
||||
|
||||
var viewboxRegex = regexp.MustCompile(`viewBox=\"([0-9\- ]+)\"`)
|
||||
var widthRegex = regexp.MustCompile(`width=\"([0-9]+)\"`)
|
||||
var heightRegex = regexp.MustCompile(`height=\"([0-9]+)\"`)
|
||||
var widthRegex = regexp.MustCompile(`width=\"([.0-9]+)\"`)
|
||||
var heightRegex = regexp.MustCompile(`height=\"([.0-9]+)\"`)
|
||||
|
||||
func Append(diagram *d2target.Diagram, ruler *textmeasure.Ruler, in []byte) []byte {
|
||||
svg := string(in)
|
||||
|
|
@ -60,7 +62,9 @@ func Append(diagram *d2target.Diagram, ruler *textmeasure.Ruler, in []byte) []by
|
|||
return in
|
||||
}
|
||||
|
||||
viewboxMatch := viewboxRegex.FindStringSubmatch(svg)
|
||||
// match 1st two viewboxes, 1st is outer fit-to-screen viewbox="0 0 innerWidth innerHeight"
|
||||
viewboxMatches := viewboxRegex.FindAllStringSubmatch(svg, 2)
|
||||
viewboxMatch := viewboxMatches[1]
|
||||
viewboxRaw := viewboxMatch[1]
|
||||
viewboxSlice := strings.Split(viewboxRaw, " ")
|
||||
viewboxPadLeft, _ := strconv.Atoi(viewboxSlice[0])
|
||||
|
|
@ -68,9 +72,13 @@ func Append(diagram *d2target.Diagram, ruler *textmeasure.Ruler, in []byte) []by
|
|||
viewboxHeight, _ := strconv.Atoi(viewboxSlice[3])
|
||||
|
||||
tl, br := diagram.BoundingBox()
|
||||
seperator := fmt.Sprintf(`<line x1="%d" y1="%d" x2="%d" y2="%d" stroke="#0A0F25" />`,
|
||||
tl.X-PAD_SIDES, br.Y+PAD_TOP, go2.IntMax(w, br.X)+PAD_SIDES, br.Y+PAD_TOP)
|
||||
appendix = seperator + appendix
|
||||
separatorEl := d2themes.NewThemableElement("line")
|
||||
separatorEl.X1 = float64(tl.X - PAD_SIDES)
|
||||
separatorEl.Y1 = float64(br.Y + PAD_TOP)
|
||||
separatorEl.X2 = float64(go2.IntMax(w, br.X) + PAD_SIDES)
|
||||
separatorEl.Y2 = float64(br.Y + PAD_TOP)
|
||||
separatorEl.Stroke = color.B2 // same as --color-border-muted in markdown
|
||||
appendix = separatorEl.Render() + appendix
|
||||
|
||||
w -= viewboxPadLeft
|
||||
w += PAD_SIDES * 2
|
||||
|
|
@ -80,16 +88,20 @@ func Append(diagram *d2target.Diagram, ruler *textmeasure.Ruler, in []byte) []by
|
|||
|
||||
viewboxHeight += h + PAD_TOP
|
||||
|
||||
newOuterViewbox := fmt.Sprintf(`viewBox="0 0 %d %d"`, viewboxWidth, viewboxHeight)
|
||||
newViewbox := fmt.Sprintf(`viewBox="%s %s %s %s"`, viewboxSlice[0], viewboxSlice[1], strconv.Itoa(viewboxWidth), strconv.Itoa(viewboxHeight))
|
||||
|
||||
widthMatch := widthRegex.FindStringSubmatch(svg)
|
||||
heightMatch := heightRegex.FindStringSubmatch(svg)
|
||||
widthMatches := widthRegex.FindAllStringSubmatch(svg, 2)
|
||||
heightMatches := heightRegex.FindAllStringSubmatch(svg, 2)
|
||||
newWidth := fmt.Sprintf(`width="%s"`, strconv.Itoa(viewboxWidth))
|
||||
newHeight := fmt.Sprintf(`height="%s"`, strconv.Itoa(viewboxHeight))
|
||||
|
||||
svg = strings.Replace(svg, viewboxMatches[0][0], newOuterViewbox, 1)
|
||||
svg = strings.Replace(svg, viewboxMatch[0], newViewbox, 1)
|
||||
svg = strings.Replace(svg, widthMatch[0], newWidth, 1)
|
||||
svg = strings.Replace(svg, heightMatch[0], newHeight, 1)
|
||||
for i := 0; i < 2; i++ {
|
||||
svg = strings.Replace(svg, widthMatches[i][0], newWidth, 1)
|
||||
svg = strings.Replace(svg, heightMatches[i][0], newHeight, 1)
|
||||
}
|
||||
|
||||
if !strings.Contains(svg, `font-family: "font-regular"`) {
|
||||
appendix += fmt.Sprintf(`<style type="text/css"><![CDATA[
|
||||
|
|
@ -114,7 +126,7 @@ func Append(diagram *d2target.Diagram, ruler *textmeasure.Ruler, in []byte) []by
|
|||
]]></style>`, d2fonts.FontEncodings[d2fonts.SourceSansPro.Font(0, d2fonts.FONT_STYLE_BOLD)])
|
||||
}
|
||||
|
||||
closingIndex := strings.LastIndex(svg, "</svg>")
|
||||
closingIndex := strings.LastIndex(svg, "</svg></svg>")
|
||||
svg = svg[:closingIndex] + appendix + svg[closingIndex:]
|
||||
|
||||
i := 1
|
||||
|
|
@ -158,7 +170,7 @@ func generateAppendix(diagram *d2target.Diagram, ruler *textmeasure.Ruler, svg s
|
|||
}
|
||||
totalHeight += SPACER
|
||||
|
||||
return fmt.Sprintf(`<g x="%d" y="%d" width="%d" height="100%%">%s</g>
|
||||
return fmt.Sprintf(`<g class="appendix" x="%d" y="%d" width="%d" height="100%%">%s</g>
|
||||
`, tl.X, br.Y, (br.X - tl.X), strings.Join(lines, "\n")), maxWidth, totalHeight
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -82,6 +82,22 @@ payment processor behind the scenes: {
|
|||
script: `x: { link: https://d2lang.com }
|
||||
y: { link: https://terrastruct.com; tooltip: Gee, I feel kind of LIGHT in the head now,\nknowing I can't make my satellite dish PAYMENTS! }
|
||||
x -> y
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "links dark",
|
||||
themeID: 200,
|
||||
script: `x: { link: https://d2lang.com }
|
||||
y: { link: https://fosny.eu; tooltip: Gee, I feel kind of LIGHT in the head now,\nknowing I can't make my satellite dish PAYMENTS! }
|
||||
x -> y
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "tooltip_fill",
|
||||
script: `x: { tooltip: Total abstinence is easier than perfect moderation }
|
||||
y: { tooltip: Gee, I feel kind of LIGHT in the head now,\nknowing I can't make my satellite dish PAYMENTS! }
|
||||
x -> y
|
||||
style.fill: PaleVioletRed
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
|
@ -89,9 +105,10 @@ x -> y
|
|||
}
|
||||
|
||||
type testCase struct {
|
||||
name string
|
||||
script string
|
||||
skip bool
|
||||
name string
|
||||
themeID int64
|
||||
script string
|
||||
skip bool
|
||||
}
|
||||
|
||||
func runa(t *testing.T, tcs []testCase) {
|
||||
|
|
@ -119,9 +136,8 @@ func run(t *testing.T, tc testCase) {
|
|||
}
|
||||
|
||||
diagram, _, err := d2lib.Compile(ctx, tc.script, &d2lib.CompileOptions{
|
||||
Ruler: ruler,
|
||||
ThemeID: 0,
|
||||
Layout: d2dagrelayout.DefaultLayout,
|
||||
Ruler: ruler,
|
||||
Layout: d2dagrelayout.DefaultLayout,
|
||||
})
|
||||
if !tassert.Nil(t, err) {
|
||||
return
|
||||
|
|
@ -131,7 +147,8 @@ func run(t *testing.T, tc testCase) {
|
|||
pathGotSVG := filepath.Join(dataPath, "sketch.got.svg")
|
||||
|
||||
svgBytes, err := d2svg.Render(diagram, &d2svg.RenderOpts{
|
||||
Pad: d2svg.DEFAULT_PADDING,
|
||||
Pad: d2svg.DEFAULT_PADDING,
|
||||
ThemeID: tc.themeID,
|
||||
})
|
||||
assert.Success(t, err)
|
||||
svgBytes = appendix.Append(diagram, ruler, svgBytes)
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 803 KiB After Width: | Height: | Size: 806 KiB |
|
Before Width: | Height: | Size: 651 KiB After Width: | Height: | Size: 654 KiB |
37
d2renderers/d2svg/appendix/testdata/links_dark/sketch.exp.svg
vendored
Normal file
|
After Width: | Height: | Size: 654 KiB |
36
d2renderers/d2svg/appendix/testdata/tooltip_fill/sketch.exp.svg
vendored
Normal file
|
After Width: | Height: | Size: 654 KiB |
|
Before Width: | Height: | Size: 650 KiB After Width: | Height: | Size: 654 KiB |
|
|
@ -3,17 +3,21 @@ package d2svg
|
|||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"oss.terrastruct.com/d2/d2target"
|
||||
"oss.terrastruct.com/d2/d2themes"
|
||||
"oss.terrastruct.com/d2/lib/geo"
|
||||
"oss.terrastruct.com/d2/lib/label"
|
||||
"oss.terrastruct.com/d2/lib/svg"
|
||||
)
|
||||
|
||||
func classHeader(shape d2target.Shape, box *geo.Box, text string, textWidth, textHeight, fontSize float64) string {
|
||||
str := fmt.Sprintf(`<rect class="class_header" x="%f" y="%f" width="%f" height="%f" fill="%s" />`,
|
||||
box.TopLeft.X, box.TopLeft.Y, box.Width, box.Height, shape.Fill)
|
||||
rectEl := d2themes.NewThemableElement("rect")
|
||||
rectEl.X, rectEl.Y = box.TopLeft.X, box.TopLeft.Y
|
||||
rectEl.Width, rectEl.Height = box.Width, box.Height
|
||||
rectEl.Fill = shape.Fill
|
||||
rectEl.ClassName = "class_header"
|
||||
str := rectEl.Render()
|
||||
|
||||
if text != "" {
|
||||
tl := label.InsideMiddleCenter.GetPointOnBox(
|
||||
|
|
@ -23,17 +27,16 @@ func classHeader(shape d2target.Shape, box *geo.Box, text string, textWidth, tex
|
|||
textHeight,
|
||||
)
|
||||
|
||||
str += fmt.Sprintf(`<text class="%s" x="%f" y="%f" style="%s">%s</text>`,
|
||||
"text-mono",
|
||||
tl.X+textWidth/2,
|
||||
tl.Y+textHeight*3/4,
|
||||
fmt.Sprintf(`text-anchor:%s;font-size:%vpx;fill:%s`,
|
||||
"middle",
|
||||
4+fontSize,
|
||||
shape.Stroke,
|
||||
),
|
||||
svg.EscapeText(text),
|
||||
textEl := d2themes.NewThemableElement("text")
|
||||
textEl.X = tl.X + textWidth/2
|
||||
textEl.Y = tl.Y + textHeight*3/4
|
||||
textEl.Fill = shape.GetFontColor()
|
||||
textEl.ClassName = "text-mono"
|
||||
textEl.Style = fmt.Sprintf(`text-anchor:%s;font-size:%vpx;`,
|
||||
"middle", 4+fontSize,
|
||||
)
|
||||
textEl.Content = svg.EscapeText(text)
|
||||
str += textEl.Render()
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
|
@ -54,33 +57,39 @@ func classRow(shape d2target.Shape, box *geo.Box, prefix, nameText, typeText str
|
|||
fontSize,
|
||||
)
|
||||
|
||||
return strings.Join([]string{
|
||||
fmt.Sprintf(`<text class="text-mono" x="%f" y="%f" style="%s">%s</text>`,
|
||||
prefixTL.X,
|
||||
prefixTL.Y+fontSize*3/4,
|
||||
fmt.Sprintf("text-anchor:%s;font-size:%vpx;fill:%s", "start", fontSize, shape.PrimaryAccentColor),
|
||||
prefix,
|
||||
),
|
||||
textEl := d2themes.NewThemableElement("text")
|
||||
textEl.X = prefixTL.X
|
||||
textEl.Y = prefixTL.Y + fontSize*3/4
|
||||
textEl.Fill = shape.PrimaryAccentColor
|
||||
textEl.ClassName = "text-mono"
|
||||
textEl.Style = fmt.Sprintf("text-anchor:%s;font-size:%vpx", "start", fontSize)
|
||||
textEl.Content = prefix
|
||||
out := textEl.Render()
|
||||
|
||||
fmt.Sprintf(`<text class="text-mono" x="%f" y="%f" style="%s">%s</text>`,
|
||||
prefixTL.X+d2target.PrefixWidth,
|
||||
prefixTL.Y+fontSize*3/4,
|
||||
fmt.Sprintf("text-anchor:%s;font-size:%vpx;fill:%s", "start", fontSize, shape.Fill),
|
||||
svg.EscapeText(nameText),
|
||||
),
|
||||
textEl.X = prefixTL.X + d2target.PrefixWidth
|
||||
textEl.Fill = shape.Fill
|
||||
textEl.Content = svg.EscapeText(nameText)
|
||||
out += textEl.Render()
|
||||
|
||||
fmt.Sprintf(`<text class="text-mono" x="%f" y="%f" style="%s">%s</text>`,
|
||||
typeTR.X,
|
||||
typeTR.Y+fontSize*3/4,
|
||||
fmt.Sprintf("text-anchor:%s;font-size:%vpx;fill:%s", "end", fontSize, shape.SecondaryAccentColor),
|
||||
svg.EscapeText(typeText),
|
||||
),
|
||||
}, "\n")
|
||||
textEl.X = typeTR.X
|
||||
textEl.Y = typeTR.Y + fontSize*3/4
|
||||
textEl.Fill = shape.SecondaryAccentColor
|
||||
textEl.Style = fmt.Sprintf("text-anchor:%s;font-size:%vpx", "end", fontSize)
|
||||
textEl.Content = svg.EscapeText(typeText)
|
||||
out += textEl.Render()
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func drawClass(writer io.Writer, targetShape d2target.Shape) {
|
||||
fmt.Fprintf(writer, `<rect class="shape" x="%d" y="%d" width="%d" height="%d" style="%s"/>`,
|
||||
targetShape.Pos.X, targetShape.Pos.Y, targetShape.Width, targetShape.Height, targetShape.CSSStyle())
|
||||
el := d2themes.NewThemableElement("rect")
|
||||
el.X = float64(targetShape.Pos.X)
|
||||
el.Y = float64(targetShape.Pos.Y)
|
||||
el.Width = float64(targetShape.Width)
|
||||
el.Height = float64(targetShape.Height)
|
||||
el.Fill, el.Stroke = d2themes.ShapeTheme(targetShape)
|
||||
el.Style = targetShape.CSSStyle()
|
||||
fmt.Fprint(writer, el.Render())
|
||||
|
||||
box := geo.NewBox(
|
||||
geo.NewPoint(float64(targetShape.Pos.X), float64(targetShape.Pos.Y)),
|
||||
|
|
@ -103,10 +112,12 @@ func drawClass(writer io.Writer, targetShape d2target.Shape) {
|
|||
rowBox.TopLeft.Y += rowHeight
|
||||
}
|
||||
|
||||
fmt.Fprintf(writer, `<line x1="%f" y1="%f" x2="%f" y2="%f" style="%s" />`,
|
||||
rowBox.TopLeft.X, rowBox.TopLeft.Y,
|
||||
rowBox.TopLeft.X+rowBox.Width, rowBox.TopLeft.Y,
|
||||
fmt.Sprintf("stroke-width:1;stroke:%v", targetShape.Fill))
|
||||
lineEl := d2themes.NewThemableElement("line")
|
||||
lineEl.X1, lineEl.Y1 = rowBox.TopLeft.X, rowBox.TopLeft.Y
|
||||
lineEl.X2, lineEl.Y2 = rowBox.TopLeft.X+rowBox.Width, rowBox.TopLeft.Y
|
||||
lineEl.Stroke = targetShape.Fill
|
||||
lineEl.Style = "stroke-width:1"
|
||||
fmt.Fprint(writer, lineEl.Render())
|
||||
|
||||
for _, m := range targetShape.Methods {
|
||||
fmt.Fprint(writer,
|
||||
|
|
|
|||
441
d2renderers/d2svg/dark_theme/dark_theme_test.go
Normal file
|
|
@ -0,0 +1,441 @@
|
|||
package dark_theme_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/xml"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"cdr.dev/slog"
|
||||
|
||||
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"
|
||||
"oss.terrastruct.com/d2/d2lib"
|
||||
"oss.terrastruct.com/d2/d2renderers/d2fonts"
|
||||
"oss.terrastruct.com/d2/d2renderers/d2svg"
|
||||
"oss.terrastruct.com/d2/lib/log"
|
||||
"oss.terrastruct.com/d2/lib/textmeasure"
|
||||
)
|
||||
|
||||
func TestDarkTheme(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tcs := []testCase{
|
||||
{
|
||||
name: "basic",
|
||||
script: `a -> b
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "child to child",
|
||||
script: `winter.snow -> summer.sun
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "animated",
|
||||
script: `winter.snow -> summer.sun -> trees -> winter.snow: { style.animated: true }
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "connection label",
|
||||
script: `a -> b: hello
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "twitter",
|
||||
script: `timeline mixer: "" {
|
||||
explanation: |md
|
||||
## **Timeline mixer**
|
||||
- Inject ads, who-to-follow, onboarding
|
||||
- Conversation module
|
||||
- Cursoring,pagination
|
||||
- Tweat deduplication
|
||||
- Served data logging
|
||||
|
|
||||
}
|
||||
People discovery: "People discovery \nservice"
|
||||
admixer: Ad mixer {
|
||||
style.fill: "#cba6f7"
|
||||
style.font-color: "#000000"
|
||||
}
|
||||
|
||||
onboarding service: "Onboarding \nservice"
|
||||
timeline mixer -> People discovery
|
||||
timeline mixer -> onboarding service
|
||||
timeline mixer -> admixer
|
||||
container0: "" {
|
||||
graphql
|
||||
comment
|
||||
tlsapi
|
||||
}
|
||||
container0.graphql: GraphQL\nFederated Strato Column {
|
||||
shape: image
|
||||
icon: https://upload.wikimedia.org/wikipedia/commons/thumb/1/17/GraphQL_Logo.svg/1200px-GraphQL_Logo.svg.png
|
||||
}
|
||||
container0.comment: |md
|
||||
## Tweet/user content hydration, visibility filtering
|
||||
|
|
||||
container0.tlsapi: TLS-API (being deprecated)
|
||||
container0.graphql -> timeline mixer
|
||||
timeline mixer <- container0.tlsapi
|
||||
twitter fe: "Twitter Frontend " {
|
||||
icon: https://icons.terrastruct.com/social/013-twitter-1.svg
|
||||
shape: image
|
||||
}
|
||||
twitter fe -> container0.graphql: iPhone web
|
||||
twitter fe -> container0.tlsapi: HTTP Android
|
||||
web: Web {
|
||||
icon: https://icons.terrastruct.com/azure/Web%20Service%20Color/App%20Service%20Domains.svg
|
||||
shape: image
|
||||
}
|
||||
|
||||
Iphone: {
|
||||
icon: 'https://ss7.vzw.com/is/image/VerizonWireless/apple-iphone-12-64gb-purple-53017-mjn13ll-a?$device-lg$'
|
||||
shape: image
|
||||
}
|
||||
Android: {
|
||||
icon: https://cdn4.iconfinder.com/data/icons/smart-phones-technologies/512/android-phone.png
|
||||
shape: image
|
||||
}
|
||||
|
||||
web -> twitter fe
|
||||
timeline scorer: "Timeline\nScorer" {
|
||||
style.fill: "#fab387"
|
||||
style.font-color: "#000000"
|
||||
}
|
||||
home ranker: Home Ranker
|
||||
|
||||
timeline service: Timeline Service
|
||||
timeline mixer -> timeline scorer: Thrift RPC
|
||||
timeline mixer -> home ranker: {
|
||||
style.stroke-dash: 4
|
||||
style.stroke: "#000E3D"
|
||||
}
|
||||
timeline mixer -> timeline service
|
||||
home mixer: Home mixer {
|
||||
# style.fill: "#c1a2f3"
|
||||
}
|
||||
container0.graphql -> home mixer: {
|
||||
style.stroke-dash: 4
|
||||
style.stroke: "#000E3D"
|
||||
}
|
||||
home mixer -> timeline scorer
|
||||
home mixer -> home ranker: {
|
||||
style.stroke-dash: 4
|
||||
style.stroke: "#000E3D"
|
||||
}
|
||||
home mixer -> timeline service
|
||||
manhattan 2: Manhattan
|
||||
gizmoduck: Gizmoduck
|
||||
socialgraph: Social graph
|
||||
tweetypie: Tweety Pie
|
||||
home mixer -> manhattan 2
|
||||
home mixer -> gizmoduck
|
||||
home mixer -> socialgraph
|
||||
home mixer -> tweetypie
|
||||
Iphone -> twitter fe
|
||||
Android -> twitter fe
|
||||
prediction service2: Prediction Service {
|
||||
shape: image
|
||||
icon: https://cdn-icons-png.flaticon.com/512/6461/6461819.png
|
||||
}
|
||||
home scorer: Home Scorer {
|
||||
style.fill: "#eba0ac"
|
||||
style.font-color: "#000000"
|
||||
}
|
||||
manhattan: Manhattan
|
||||
memcache: Memcache {
|
||||
icon: https://d1q6f0aelx0por.cloudfront.net/product-logos/de041504-0ddb-43f6-b89e-fe04403cca8d-memcached.png
|
||||
}
|
||||
|
||||
fetch: Fetch {
|
||||
style.multiple: true
|
||||
shape: step
|
||||
}
|
||||
feature: Feature {
|
||||
style.multiple: true
|
||||
shape: step
|
||||
}
|
||||
scoring: Scoring {
|
||||
style.multiple: true
|
||||
shape: step
|
||||
}
|
||||
fetch -> feature
|
||||
feature -> scoring
|
||||
|
||||
prediction service: Prediction Service {
|
||||
shape: image
|
||||
icon: https://cdn-icons-png.flaticon.com/512/6461/6461819.png
|
||||
}
|
||||
scoring -> prediction service
|
||||
fetch -> container2.crmixer
|
||||
|
||||
home scorer -> manhattan: ""
|
||||
|
||||
home scorer -> memcache: ""
|
||||
home scorer -> prediction service2
|
||||
home ranker -> home scorer
|
||||
home ranker -> container2.crmixer: Candidate Fetch
|
||||
container2: "" {
|
||||
style.stroke: "#b4befe"
|
||||
style.fill: "#000000"
|
||||
crmixer: CrMixer {
|
||||
style.fill: "#11111b"
|
||||
style.font-color: "#cdd6f4"
|
||||
}
|
||||
earlybird: EarlyBird
|
||||
utag: Utag
|
||||
space: Space
|
||||
communities: Communities
|
||||
}
|
||||
etc: ...etc
|
||||
|
||||
home scorer -> etc: Feature Hydration
|
||||
|
||||
feature -> manhattan
|
||||
feature -> memcache
|
||||
feature -> etc: Candidate sources
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "all_shapes",
|
||||
script: `
|
||||
rectangle: {shape: "rectangle"}
|
||||
square: {shape: "square"}
|
||||
page: {shape: "page"}
|
||||
parallelogram: {shape: "parallelogram"}
|
||||
document: {shape: "document"}
|
||||
cylinder: {shape: "cylinder"}
|
||||
queue: {shape: "queue"}
|
||||
package: {shape: "package"}
|
||||
step: {shape: "step"}
|
||||
callout: {shape: "callout"}
|
||||
stored_data: {shape: "stored_data"}
|
||||
person: {shape: "person"}
|
||||
diamond: {shape: "diamond"}
|
||||
oval: {shape: "oval"}
|
||||
circle: {shape: "circle"}
|
||||
hexagon: {shape: "hexagon"}
|
||||
cloud: {shape: "cloud"}
|
||||
|
||||
rectangle -> square -> page
|
||||
parallelogram -> document -> cylinder
|
||||
queue -> package -> step
|
||||
callout -> stored_data -> person
|
||||
diamond -> oval -> circle
|
||||
hexagon -> cloud
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "sql_tables",
|
||||
script: `users: {
|
||||
shape: sql_table
|
||||
id: int
|
||||
name: string
|
||||
email: string
|
||||
password: string
|
||||
last_login: datetime
|
||||
}
|
||||
|
||||
products: {
|
||||
shape: sql_table
|
||||
id: int
|
||||
price: decimal
|
||||
sku: string
|
||||
name: string
|
||||
}
|
||||
|
||||
orders: {
|
||||
shape: sql_table
|
||||
id: int
|
||||
user_id: int
|
||||
product_id: int
|
||||
}
|
||||
|
||||
shipments: {
|
||||
shape: sql_table
|
||||
id: int
|
||||
order_id: int
|
||||
tracking_number: string {constraint: primary_key}
|
||||
status: string
|
||||
}
|
||||
|
||||
users.id <-> orders.user_id
|
||||
products.id <-> orders.product_id
|
||||
shipments.order_id <-> orders.id`,
|
||||
},
|
||||
{
|
||||
name: "class",
|
||||
script: `manager: BatchManager {
|
||||
shape: class
|
||||
-num: int
|
||||
-timeout: int
|
||||
-pid
|
||||
|
||||
+getStatus(): Enum
|
||||
+getJobs(): "Job[]"
|
||||
+setTimeout(seconds int)
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "arrowheads",
|
||||
script: `
|
||||
a: ""
|
||||
b: ""
|
||||
a.1 -- b.1: none
|
||||
a.2 <-> b.2: arrow {
|
||||
source-arrowhead.shape: arrow
|
||||
target-arrowhead.shape: arrow
|
||||
}
|
||||
a.3 <-> b.3: triangle {
|
||||
source-arrowhead.shape: triangle
|
||||
target-arrowhead.shape: triangle
|
||||
}
|
||||
a.4 <-> b.4: diamond {
|
||||
source-arrowhead.shape: diamond
|
||||
target-arrowhead.shape: diamond
|
||||
}
|
||||
a.5 <-> b.5: diamond filled {
|
||||
source-arrowhead: {
|
||||
shape: diamond
|
||||
style.filled: true
|
||||
}
|
||||
target-arrowhead: {
|
||||
shape: diamond
|
||||
style.filled: true
|
||||
}
|
||||
}
|
||||
a.6 <-> b.6: cf-many {
|
||||
source-arrowhead.shape: cf-many
|
||||
target-arrowhead.shape: cf-many
|
||||
}
|
||||
a.7 <-> b.7: cf-many-required {
|
||||
source-arrowhead.shape: cf-many-required
|
||||
target-arrowhead.shape: cf-many-required
|
||||
}
|
||||
a.8 <-> b.8: cf-one {
|
||||
source-arrowhead.shape: cf-one
|
||||
target-arrowhead.shape: cf-one
|
||||
}
|
||||
a.9 <-> b.9: cf-one-required {
|
||||
source-arrowhead.shape: cf-one-required
|
||||
target-arrowhead.shape: cf-one-required
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "opacity",
|
||||
script: `x.style.opacity: 0.4
|
||||
y: |md
|
||||
linux: because a PC is a terrible thing to waste
|
||||
| {
|
||||
style.opacity: 0.4
|
||||
}
|
||||
x -> a: {
|
||||
label: You don't have to know how the computer works,\njust how to work the computer.
|
||||
style.opacity: 0.4
|
||||
}
|
||||
users: {
|
||||
shape: sql_table
|
||||
last_login: datetime
|
||||
style.opacity: 0.4
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "overlay",
|
||||
script: `bright: {
|
||||
style.stroke: "#000"
|
||||
style.font-color: "#000"
|
||||
style.fill: "#fff"
|
||||
}
|
||||
normal: {
|
||||
style.stroke: "#000"
|
||||
style.font-color: "#000"
|
||||
style.fill: "#ccc"
|
||||
}
|
||||
dark: {
|
||||
style.stroke: "#000"
|
||||
style.font-color: "#fff"
|
||||
style.fill: "#555"
|
||||
}
|
||||
darker: {
|
||||
style.stroke: "#000"
|
||||
style.font-color: "#fff"
|
||||
style.fill: "#000"
|
||||
}
|
||||
`,
|
||||
},
|
||||
}
|
||||
runa(t, tcs)
|
||||
}
|
||||
|
||||
type testCase struct {
|
||||
name string
|
||||
script string
|
||||
skip bool
|
||||
}
|
||||
|
||||
func runa(t *testing.T, tcs []testCase) {
|
||||
for _, tc := range tcs {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if tc.skip {
|
||||
t.Skip()
|
||||
}
|
||||
t.Parallel()
|
||||
|
||||
run(t, tc)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func run(t *testing.T, tc testCase) {
|
||||
ctx := context.Background()
|
||||
ctx = log.WithTB(ctx, t, nil)
|
||||
ctx = log.Leveled(ctx, slog.LevelDebug)
|
||||
|
||||
ruler, err := textmeasure.NewRuler()
|
||||
if !tassert.Nil(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
diagram, _, err := d2lib.Compile(ctx, tc.script, &d2lib.CompileOptions{
|
||||
Ruler: ruler,
|
||||
Layout: d2dagrelayout.DefaultLayout,
|
||||
FontFamily: go2.Pointer(d2fonts.HandDrawn),
|
||||
})
|
||||
if !tassert.Nil(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
dataPath := filepath.Join("testdata", strings.TrimPrefix(t.Name(), "TestDarkTheme/"))
|
||||
pathGotSVG := filepath.Join(dataPath, "dark_theme.got.svg")
|
||||
|
||||
svgBytes, err := d2svg.Render(diagram, &d2svg.RenderOpts{
|
||||
Pad: d2svg.DEFAULT_PADDING,
|
||||
ThemeID: 200,
|
||||
})
|
||||
assert.Success(t, err)
|
||||
err = os.MkdirAll(dataPath, 0755)
|
||||
assert.Success(t, err)
|
||||
err = ioutil.WriteFile(pathGotSVG, svgBytes, 0600)
|
||||
assert.Success(t, err)
|
||||
defer os.Remove(pathGotSVG)
|
||||
|
||||
var xmlParsed interface{}
|
||||
err = xml.Unmarshal(svgBytes, &xmlParsed)
|
||||
assert.Success(t, err)
|
||||
|
||||
err = diff.Testdata(filepath.Join(dataPath, "dark_theme"), ".svg", svgBytes)
|
||||
assert.Success(t, err)
|
||||
}
|
||||
23
d2renderers/d2svg/dark_theme/testdata/all_shapes/dark_theme.exp.svg
vendored
Normal file
|
After Width: | Height: | Size: 196 KiB |
36
d2renderers/d2svg/dark_theme/testdata/animated/dark_theme.exp.svg
vendored
Normal file
|
After Width: | Height: | Size: 239 KiB |
38
d2renderers/d2svg/dark_theme/testdata/arrowheads/dark_theme.exp.svg
vendored
Normal file
|
After Width: | Height: | Size: 252 KiB |
23
d2renderers/d2svg/dark_theme/testdata/basic/dark_theme.exp.svg
vendored
Normal file
|
After Width: | Height: | Size: 188 KiB |
30
d2renderers/d2svg/dark_theme/testdata/child_to_child/dark_theme.exp.svg
vendored
Normal file
|
After Width: | Height: | Size: 237 KiB |
23
d2renderers/d2svg/dark_theme/testdata/class/dark_theme.exp.svg
vendored
Normal file
|
After Width: | Height: | Size: 188 KiB |