Merge branch 'master' into update-contributing
2
.github/workflows/ci.yml
vendored
|
|
@ -27,7 +27,7 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- run: git submodule update --init
|
||||
- run: COLOR=1 ./ci/sub/nofixups.sh
|
||||
- run: COLOR=1 ./ci/sub/bin/nofixups.sh
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets._GITHUB_TOKEN }}
|
||||
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||
|
|
|
|||
6
.prettierignore
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
d2layouts/d2dagrelayout/dagre.js
|
||||
d2layouts/d2elklayout/setup.js
|
||||
d2renderers/d2latex/mathjax.js
|
||||
d2renderers/d2latex/polyfills.js
|
||||
d2renderers/d2latex/setup.js
|
||||
lib/png/generate_png.js
|
||||
2
Makefile
|
|
@ -5,7 +5,7 @@ all: fmt gen lint build test
|
|||
|
||||
.PHONY: fmt
|
||||
fmt:
|
||||
prefix "$@" ./ci/fmt.sh
|
||||
prefix "$@" ./ci/sub/bin/fmt.sh
|
||||
.PHONY: gen
|
||||
gen:
|
||||
prefix "$@" ./ci/gen.sh
|
||||
|
|
|
|||
47
README.md
|
|
@ -4,14 +4,20 @@
|
|||
A modern diagram scripting language that turns text to diagrams.
|
||||
</h2>
|
||||
|
||||
[Language docs](https://d2lang.com) | [Cheat sheet](./docs/assets/cheat_sheet.pdf) | [Comparisons](https://text-to-diagram.com)
|
||||
|
||||
[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)
|
||||
[](https://github.com/terrastruct/d2/actions/workflows/daily.yml)
|
||||
[](https://github.com/terrastruct/d2/releases)
|
||||
[](https://discord.gg/NF6X8K4eDq)
|
||||
[](https://twitter.com/terrastruct)
|
||||
[](./LICENSE.txt)
|
||||
|
||||
<a href="https://play.d2lang.com">
|
||||
<img src="./docs/assets/playground_button.png" alt="D2 Playground button" width="200" />
|
||||
</a>
|
||||
|
||||
https://user-images.githubusercontent.com/3120367/206125010-bd1fea8e-248a-43e7-8f85-0bbfca0c6e2a.mp4
|
||||
|
||||
</div>
|
||||
|
|
@ -32,9 +38,8 @@ https://user-images.githubusercontent.com/3120367/206125010-bd1fea8e-248a-43e7-8
|
|||
- <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="#vscode-extension" id="toc-vscode-extension">VSCode extension</a>
|
||||
- <a href="#vim-extension" id="toc-vim-extension">Vim extension</a>
|
||||
- <a href="#language-docs" id="toc-language-docs">Language docs</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>
|
||||
|
||||
|
|
@ -140,7 +145,7 @@ one, please see [./d2renderers/d2fonts](./d2renderers/d2fonts).
|
|||
|
||||
## Export file types
|
||||
|
||||
D2 currently supports SVG exports. More coming soon.
|
||||
D2 currently supports SVG and PNG exports. More coming soon.
|
||||
|
||||
## Language tooling
|
||||
|
||||
|
|
@ -186,21 +191,37 @@ Open sourced under the Mozilla Public License 2.0. See [./LICENSE.txt](./LICENSE
|
|||
|
||||
## Related
|
||||
|
||||
### VSCode extension
|
||||
We are constantly working on new plugins, integrations, extensions. Contributions are
|
||||
welcome in any official or community plugins. If you have somewhere in your workflow that
|
||||
you want to use D2, feel free to open a discussion. We have limited bandwidth and usually
|
||||
choose the most high-demand ones to work on. If you make something cool with D2 yourself,
|
||||
let us know and we'll be happy to include it here!
|
||||
|
||||
[https://github.com/terrastruct/d2-vscode](https://github.com/terrastruct/d2-vscode)
|
||||
### Official plugins
|
||||
|
||||
### Vim extension
|
||||
- **VSCode extension**: [https://github.com/terrastruct/d2-vscode](https://github.com/terrastruct/d2-vscode)
|
||||
- **Vim extension**: [https://github.com/terrastruct/d2-vim](https://github.com/terrastruct/d2-vim)
|
||||
- **Obsidian plugin**: [https://github.com/terrastruct/d2-obsidian](https://github.com/terrastruct/d2-obsidian)
|
||||
- **Slack app**: [https://d2lang.com/tour/slack](https://d2lang.com/tour/slack)
|
||||
- **Discord plugin**: [https://d2lang.com/tour/discord](https://d2lang.com/tour/discord)
|
||||
|
||||
[https://github.com/terrastruct/d2-vim](https://github.com/terrastruct/d2-vim)
|
||||
### Community plugins
|
||||
|
||||
### Language docs
|
||||
|
||||
[https://github.com/terrastruct/d2-docs](https://github.com/terrastruct/d2-docs)
|
||||
- **Tree-sitter grammar**: [https://github.com/pleshevskiy/tree-sitter-d2](https://github.com/pleshevskiy/tree-sitter-d2)
|
||||
- **Emacs major mode**: [https://github.com/andorsk/d2-mode](https://github.com/andorsk/d2-mode)
|
||||
- **Goldmark extension**: [https://github.com/FurqanSoftware/goldmark-d2](https://github.com/FurqanSoftware/goldmark-d2)
|
||||
- **Telegram bot**: [https://github.com/meinside/telegram-d2-bot](https://github.com/meinside/telegram-d2-bot)
|
||||
- **Postgres importer**: [https://github.com/zekenie/d2-erd-from-postgres](https://github.com/zekenie/d2-erd-from-postgres)
|
||||
- **Structurizr to D2 exporter**: [https://github.com/goto1134/structurizr-d2-exporter](https://github.com/goto1134/structurizr-d2-exporter)
|
||||
- **MdBook preprocessor**: [https://github.com/danieleades/mdbook-d2](https://github.com/danieleades/mdbook-d2)
|
||||
- **D2 org-mode support**: [https://github.com/xcapaldi/ob-d2](https://github.com/xcapaldi/ob-d2)
|
||||
- **Python D2 diagram builder**: [https://github.com/MrBlenny/py-d2](https://github.com/MrBlenny/py-d2)
|
||||
|
||||
### Misc
|
||||
|
||||
- [https://github.com/terrastruct/text-to-diagram-site](https://github.com/terrastruct/text-to-diagram-site)
|
||||
- **Comparison site**: [https://github.com/terrastruct/text-to-diagram-site](https://github.com/terrastruct/text-to-diagram-site)
|
||||
- **Playground**: [https://github.com/terrastruct/d2-playground](https://github.com/terrastruct/d2-playground)
|
||||
- **Language docs**: [https://github.com/terrastruct/d2-docs](https://github.com/terrastruct/d2-docs)
|
||||
|
||||
## FAQ
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ set -eu
|
|||
|
||||
export REPORT_OUTPUT="out/e2e_report.html"
|
||||
rm -f $REPORT_OUTPUT
|
||||
export E2E_REPORT=1
|
||||
|
||||
FORCE_COLOR=1 DEBUG=1 go run ./e2etests/report/main.go "$@";
|
||||
|
||||
|
|
|
|||
12
ci/fmt.sh
|
|
@ -1,12 +0,0 @@
|
|||
#!/bin/sh
|
||||
set -eu
|
||||
. "$(dirname "$0")/sub/lib.sh"
|
||||
cd -- "$(dirname "$0")/.."
|
||||
|
||||
if is_changed README.md; then
|
||||
sh_c tocsubst --skip 1 README.md
|
||||
fi
|
||||
if is_changed docs/INSTALL.md; then
|
||||
sh_c tocsubst --skip 1 docs/INSTALL.md
|
||||
fi
|
||||
./ci/sub/fmt/make.sh
|
||||
18
ci/release/Dockerfile
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# https://hub.docker.com/repository/docker/terrastruct/d2
|
||||
FROM debian:latest
|
||||
|
||||
ARG TARGETARCH
|
||||
|
||||
COPY ./d2-*-linux-$TARGETARCH.tar.gz /tmp
|
||||
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
|
||||
|
||||
WORKDIR /root/src
|
||||
EXPOSE 8080
|
||||
ENV PORT 8080
|
||||
ENV HOST 0.0.0.0
|
||||
ENV BROWSER false
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/d2"]
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
## _install.sh
|
||||
|
||||
The template for the install script in the root of the repository.
|
||||
The template for the install script in the root of the d2 repository.
|
||||
|
||||
### gen_install.sh
|
||||
|
||||
|
|
@ -23,12 +23,12 @@ it depends on from ../sub/lib.
|
|||
> variables as we must compile d2 directly on each release target to include dagre.
|
||||
> See https://github.com/terrastruct/d2/issues/31
|
||||
|
||||
Use `--host-only` to build only the release for the host's OS-ARCH pair.
|
||||
Use `--host-only` to build only the release for the host's `$OS-$ARCH` pair.
|
||||
|
||||
### build_docker.sh
|
||||
|
||||
Helper script called by build.sh to build D2 on each linux runner inside Docker.
|
||||
The Dockerfile is in ./builders/Dockerfile
|
||||
The Dockerfile is in ./linux/Dockerfile
|
||||
|
||||
### _build.sh
|
||||
|
||||
|
|
|
|||
|
|
@ -3,20 +3,25 @@ set -eu
|
|||
cd -- "$(dirname "$0")/../.."
|
||||
. ./ci/sub/lib.sh
|
||||
|
||||
sh_c rm -Rf "$HW_BUILD_DIR"
|
||||
sh_c mkdir -p "$HW_BUILD_DIR"
|
||||
sh_c rsync --recursive --perms --delete \
|
||||
--human-readable --copy-links ./ci/release/template/ "$HW_BUILD_DIR/"
|
||||
VERSION=$VERSION sh_c eval "'$HW_BUILD_DIR/README.md.sh'" \> "'$HW_BUILD_DIR/README.md'"
|
||||
sh_c rm -f "$HW_BUILD_DIR/README.md.sh"
|
||||
sh_c find "$HW_BUILD_DIR" -exec touch {} \\\;
|
||||
sh_c cp ./ci/release/template/LICENSE.txt "$HW_BUILD_DIR"
|
||||
sh_c cp ./ci/release/template/Makefile "$HW_BUILD_DIR"
|
||||
sh_c cp -R ./ci/release/template/man "$HW_BUILD_DIR"
|
||||
sh_c cp -R ./ci/release/template/scripts "$HW_BUILD_DIR"
|
||||
sh_c VERSION="$VERSION" ./ci/release/template/README.md.sh \> "'$HW_BUILD_DIR/README.md'"
|
||||
|
||||
ensure_goos
|
||||
ensure_goarch
|
||||
sh_c mkdir -p "$HW_BUILD_DIR/bin"
|
||||
sh_c CGO_ENABLED=0 go build -trimpath \
|
||||
sh_c GOOS="$GOOS" GOARCH="$GOARCH" CGO_ENABLED=0 go build -trimpath \
|
||||
-ldflags "'-X oss.terrastruct.com/d2/lib/version.Version=$VERSION'" \
|
||||
-o "$HW_BUILD_DIR/bin/d2" .
|
||||
|
||||
if [ "$GOOS" = windows ]; then
|
||||
sh_c mv "$HW_BUILD_DIR/bin/d2" "$HW_BUILD_DIR/bin/d2.exe"
|
||||
fi
|
||||
|
||||
ARCHIVE=$PWD/$ARCHIVE
|
||||
cd "$(dirname "$HW_BUILD_DIR")"
|
||||
sh_c tar -czf "$ARCHIVE" "$(basename "$HW_BUILD_DIR")"
|
||||
|
|
|
|||
|
|
@ -347,7 +347,7 @@ install_d2_standalone() {
|
|||
|
||||
install_d2_brew() {
|
||||
header "installing d2 with homebrew"
|
||||
sh_c brew tap terrastruct/d2
|
||||
sh_c brew update
|
||||
sh_c brew install d2
|
||||
}
|
||||
|
||||
|
|
@ -390,8 +390,8 @@ install_tala_standalone() {
|
|||
|
||||
install_tala_brew() {
|
||||
header "installing tala with homebrew"
|
||||
sh_c brew tap terrastruct/d2
|
||||
sh_c brew install tala
|
||||
sh_c brew update
|
||||
sh_c brew install terrastruct/tap/tala
|
||||
}
|
||||
|
||||
uninstall() {
|
||||
|
|
@ -502,11 +502,6 @@ fetch_gh() {
|
|||
sh_c mv "$file.inprogress" "$file"
|
||||
}
|
||||
|
||||
brew() {
|
||||
# Makes brew sane.
|
||||
HOMEBREW_NO_INSTALL_CLEANUP=1 HOMEBREW_NO_AUTO_UPDATE=1 command brew "$@"
|
||||
}
|
||||
|
||||
# The main function does more than provide organization. It provides robustness in that if
|
||||
# the install script was to only partial download into sh, sh will not execute it because
|
||||
# main is not invoked until the very last byte.
|
||||
|
|
|
|||
545
ci/release/aws/ensure.sh
Executable file
|
|
@ -0,0 +1,545 @@
|
|||
#!/bin/sh
|
||||
set -eu
|
||||
. "$(dirname "$0")/../../../ci/sub/lib.sh"
|
||||
cd -- "$(dirname "$0")/../../.."
|
||||
|
||||
help() {
|
||||
cat <<EOF
|
||||
usage: $0 [--dry-run] [--skip-create] [--skip-init] [--copy-id=id.pub]
|
||||
[--run=jobregex]
|
||||
|
||||
$0 creates and ensures the d2 builders in AWS.
|
||||
EOF
|
||||
}
|
||||
|
||||
main() {
|
||||
while flag_parse "$@"; do
|
||||
case "$FLAG" in
|
||||
h|help)
|
||||
help
|
||||
return 0
|
||||
;;
|
||||
x)
|
||||
flag_noarg && shift "$FLAGSHIFT"
|
||||
set -x
|
||||
export TRACE=1
|
||||
;;
|
||||
dry-run)
|
||||
flag_noarg && shift "$FLAGSHIFT"
|
||||
export DRY_RUN=1
|
||||
;;
|
||||
copy-id)
|
||||
flag_nonemptyarg && shift "$FLAGSHIFT"
|
||||
ID_PUB_PATH=$FLAGARG
|
||||
;;
|
||||
run)
|
||||
flag_reqarg && shift "$FLAGSHIFT"
|
||||
JOBFILTER="$FLAGARG"
|
||||
;;
|
||||
*)
|
||||
flag_errusage "unrecognized flag $FLAGRAW"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
shift "$FLAGSHIFT"
|
||||
if [ $# -gt 0 ]; then
|
||||
flag_errusage "no arguments are accepted"
|
||||
fi
|
||||
if [ -z "${ID_PUB_PATH-}" ]; then
|
||||
flag_errusage "--copy-id is required"
|
||||
fi
|
||||
|
||||
JOBNAME=create runjob_filter create_remote_hosts
|
||||
JOBNAME=init runjob_filter init_remote_hosts
|
||||
|
||||
FGCOLOR=2 header summary
|
||||
echo "export CI_D2_LINUX_AMD64=$CI_D2_LINUX_AMD64"
|
||||
echo "export CI_D2_LINUX_ARM64=$CI_D2_LINUX_ARM64"
|
||||
echo "export CI_D2_MACOS_AMD64=$CI_D2_MACOS_AMD64"
|
||||
echo "export CI_D2_MACOS_ARM64=$CI_D2_MACOS_ARM64"
|
||||
echo "export CI_D2_WINDOWS_AMD64=$CI_D2_WINDOWS_AMD64"
|
||||
}
|
||||
|
||||
create_remote_hosts() {
|
||||
bigheader create_remote_hosts
|
||||
|
||||
KEY_NAME=$(aws ec2 describe-key-pairs | jq -r .KeyPairs[0].KeyName)
|
||||
KEY_NAME_WINDOWS=windows
|
||||
VPC_ID=$(aws ec2 describe-vpcs | jq -r .Vpcs[0].VpcId)
|
||||
|
||||
JOBNAME=$JOBNAME/security-groups runjob_filter create_security_groups
|
||||
JOBNAME=$JOBNAME/linux/amd64 runjob_filter create_linux_amd64
|
||||
JOBNAME=$JOBNAME/linux/arm64 runjob_filter create_linux_arm64
|
||||
JOBNAME=$JOBNAME/macos/amd64 runjob_filter create_macos_amd64
|
||||
JOBNAME=$JOBNAME/macos/arm64 runjob_filter create_macos_arm64
|
||||
JOBNAME=$JOBNAME/windows/amd64 runjob_filter create_windows_amd64
|
||||
}
|
||||
|
||||
create_security_groups() {
|
||||
header security-group
|
||||
SG_ID=$(aws ec2 describe-security-groups --group-names ssh 2>/dev/null \
|
||||
| jq -r .SecurityGroups[0].GroupId)
|
||||
if [ -z "$SG_ID" ]; then
|
||||
SG_ID=$(sh_c aws ec2 create-security-group \
|
||||
--group-name ssh \
|
||||
--description ssh \
|
||||
--vpc-id "$VPC_ID" | jq -r .GroupId)
|
||||
fi
|
||||
|
||||
header security-group-ingress
|
||||
SG_RULES_COUNT=$(aws ec2 describe-security-groups --group-names ssh \
|
||||
| jq -r '.SecurityGroups[0].IpPermissions | length')
|
||||
if [ "$SG_RULES_COUNT" -eq 0 ]; then
|
||||
sh_c aws ec2 authorize-security-group-ingress \
|
||||
--group-id "$SG_ID" \
|
||||
--protocol tcp \
|
||||
--port 22 \
|
||||
--cidr 0.0.0.0/0 >/dev/null
|
||||
fi
|
||||
|
||||
header windows-security-group
|
||||
SG_ID=$(aws ec2 describe-security-groups --group-names windows 2>/dev/null \
|
||||
| jq -r .SecurityGroups[0].GroupId)
|
||||
if [ -z "$SG_ID" ]; then
|
||||
SG_ID=$(sh_c aws ec2 create-security-group \
|
||||
--group-name windows \
|
||||
--description windows \
|
||||
--vpc-id "$VPC_ID" | jq -r .GroupId)
|
||||
fi
|
||||
|
||||
header windows-security-group-ingress
|
||||
SG_RULES_COUNT=$(aws ec2 describe-security-groups --group-names windows \
|
||||
| jq -r '.SecurityGroups[0].IpPermissions | length')
|
||||
if [ "$SG_RULES_COUNT" -ne 2 ]; then
|
||||
sh_c aws ec2 authorize-security-group-ingress \
|
||||
--group-id "$SG_ID" \
|
||||
--protocol tcp \
|
||||
--port 22 \
|
||||
--cidr 0.0.0.0/0 >/dev/null
|
||||
sh_c aws ec2 authorize-security-group-ingress \
|
||||
--group-id "$SG_ID" \
|
||||
--protocol tcp \
|
||||
--port 3389 \
|
||||
--cidr 0.0.0.0/0 >/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
create_linux_amd64() {
|
||||
header linux-amd64
|
||||
REMOTE_NAME=ci-d2-linux-amd64
|
||||
state=$(aws ec2 describe-instances --filters \
|
||||
'Name=instance-state-name,Values=pending,running,stopping,stopped' 'Name=tag:Name,Values=ci-d2-linux-amd64' \
|
||||
| jq -r '.Reservations[].Instances[].State.Name')
|
||||
if [ -z "$state" ]; then
|
||||
sh_c aws ec2 run-instances \
|
||||
--image-id=ami-0ecc74eca1d66d8a6 \
|
||||
--count=1 \
|
||||
--instance-type=t3.small \
|
||||
--security-groups=ssh \
|
||||
"--key-name=$KEY_NAME" \
|
||||
--iam-instance-profile 'Name=AmazonSSMRoleForInstancesQuickSetup' \
|
||||
--block-device-mappings '"DeviceName=/dev/sda1,Ebs={VolumeSize=64,VolumeType=gp3}"' \
|
||||
--tag-specifications '"ResourceType=instance,Tags=[{Key=Name,Value=ci-d2-linux-amd64}]"' \
|
||||
'"ResourceType=volume,Tags=[{Key=Name,Value=ci-d2-linux-amd64}]"' >/dev/null
|
||||
fi
|
||||
wait_remote_host_ip
|
||||
log "CI_D2_LINUX_AMD64=ubuntu@$ip"
|
||||
export CI_D2_LINUX_AMD64=ubuntu@$ip
|
||||
}
|
||||
|
||||
create_linux_arm64() {
|
||||
header linux-arm64
|
||||
REMOTE_NAME=ci-d2-linux-arm64
|
||||
state=$(aws ec2 describe-instances --filters \
|
||||
'Name=instance-state-name,Values=pending,running,stopping,stopped' 'Name=tag:Name,Values=ci-d2-linux-arm64' \
|
||||
| jq -r '.Reservations[].Instances[].State.Name')
|
||||
if [ -z "$state" ]; then
|
||||
sh_c aws ec2 run-instances \
|
||||
--image-id=ami-06e2dea2cdda3acda \
|
||||
--count=1 \
|
||||
--instance-type=t4g.small \
|
||||
--security-groups=ssh \
|
||||
"--key-name=$KEY_NAME" \
|
||||
--iam-instance-profile 'Name=AmazonSSMRoleForInstancesQuickSetup' \
|
||||
--block-device-mappings '"DeviceName=/dev/sda1,Ebs={VolumeSize=64,VolumeType=gp3}"' \
|
||||
--tag-specifications '"ResourceType=instance,Tags=[{Key=Name,Value=ci-d2-linux-arm64}]"' \
|
||||
'"ResourceType=volume,Tags=[{Key=Name,Value=ci-d2-linux-arm64}]"' >/dev/null
|
||||
fi
|
||||
wait_remote_host_ip
|
||||
log "CI_D2_LINUX_ARM64=ubuntu@$ip"
|
||||
export CI_D2_LINUX_ARM64=ubuntu@$ip
|
||||
}
|
||||
|
||||
create_macos_amd64() {
|
||||
header macos-amd64-host
|
||||
MACOS_AMD64_ID=$(aws ec2 describe-hosts --filter 'Name=state,Values=pending,available' 'Name=tag:Name,Values=ci-d2-macos-amd64' | jq -r '.Hosts[].HostId')
|
||||
if [ -z "$MACOS_AMD64_ID" ]; then
|
||||
MACOS_AMD64_ID=$(sh_c aws ec2 allocate-hosts --instance-type mac1.metal --quantity 1 --availability-zone us-west-2a \
|
||||
--tag-specifications '"ResourceType=dedicated-host,Tags=[{Key=Name,Value=ci-d2-macos-amd64}]"' \
|
||||
| jq -r .HostIds[0])
|
||||
fi
|
||||
|
||||
header macos-amd64
|
||||
REMOTE_NAME=ci-d2-macos-amd64
|
||||
state=$(aws ec2 describe-instances --filters \
|
||||
'Name=instance-state-name,Values=pending,running,stopping,stopped' 'Name=tag:Name,Values=ci-d2-macos-amd64' \
|
||||
| jq -r '.Reservations[].Instances[].State.Name')
|
||||
if [ -z "$state" ]; then
|
||||
sh_c aws ec2 run-instances \
|
||||
--image-id=ami-0dd2ded7568750663 \
|
||||
--count=1 \
|
||||
--instance-type=mac1.metal \
|
||||
--security-groups=ssh \
|
||||
"--key-name=$KEY_NAME" \
|
||||
--iam-instance-profile 'Name=AmazonSSMRoleForInstancesQuickSetup' \
|
||||
--placement "Tenancy=host,HostId=$MACOS_AMD64_ID" \
|
||||
--block-device-mappings '"DeviceName=/dev/sda1,Ebs={VolumeSize=100,VolumeType=gp3}"' \
|
||||
--tag-specifications '"ResourceType=instance,Tags=[{Key=Name,Value=ci-d2-macos-amd64}]"' \
|
||||
'"ResourceType=volume,Tags=[{Key=Name,Value=ci-d2-macos-amd64}]"' >/dev/null
|
||||
fi
|
||||
wait_remote_host_ip
|
||||
log "CI_D2_MACOS_AMD64=ec2-user@$ip"
|
||||
export CI_D2_MACOS_AMD64=ec2-user@$ip
|
||||
}
|
||||
|
||||
create_macos_arm64() {
|
||||
header macos-arm64-host
|
||||
MACOS_ARM64_ID=$(aws ec2 describe-hosts --filter 'Name=state,Values=pending,available' 'Name=tag:Name,Values=ci-d2-macos-arm64' | jq -r '.Hosts[].HostId')
|
||||
if [ -z "$MACOS_ARM64_ID" ]; then
|
||||
MACOS_ARM64_ID=$(sh_c aws ec2 allocate-hosts --instance-type mac2.metal --quantity 1 --availability-zone us-west-2a \
|
||||
--tag-specifications '"ResourceType=dedicated-host,Tags=[{Key=Name,Value=ci-d2-macos-arm64}]"' \
|
||||
| jq -r .HostIds[0])
|
||||
fi
|
||||
|
||||
header macos-arm64
|
||||
REMOTE_NAME=ci-d2-macos-arm64
|
||||
state=$(aws ec2 describe-instances --filters \
|
||||
'Name=instance-state-name,Values=pending,running,stopping,stopped' 'Name=tag:Name,Values=ci-d2-macos-arm64' \
|
||||
| jq -r '.Reservations[].Instances[].State.Name')
|
||||
if [ -z "$state" ]; then
|
||||
sh_c aws ec2 run-instances \
|
||||
--image-id=ami-0af0516ff2c43dbbe \
|
||||
--count=1 \
|
||||
--instance-type=mac2.metal \
|
||||
--security-groups=ssh \
|
||||
"--key-name=$KEY_NAME" \
|
||||
--iam-instance-profile 'Name=AmazonSSMRoleForInstancesQuickSetup' \
|
||||
--placement "Tenancy=host,HostId=$MACOS_ARM64_ID" \
|
||||
--block-device-mappings '"DeviceName=/dev/sda1,Ebs={VolumeSize=100,VolumeType=gp3}"' \
|
||||
--tag-specifications '"ResourceType=instance,Tags=[{Key=Name,Value=ci-d2-macos-arm64}]"' \
|
||||
'"ResourceType=volume,Tags=[{Key=Name,Value=ci-d2-macos-arm64}]"' >/dev/null
|
||||
fi
|
||||
wait_remote_host_ip
|
||||
log "CI_D2_MACOS_ARM64=ec2-user@$ip"
|
||||
export CI_D2_MACOS_ARM64=ec2-user@$ip
|
||||
}
|
||||
|
||||
create_windows_amd64() {
|
||||
header windows-amd64
|
||||
REMOTE_NAME=ci-d2-windows-amd64
|
||||
state=$(aws ec2 describe-instances --filters \
|
||||
'Name=instance-state-name,Values=pending,running,stopping,stopped' "Name=tag:Name,Values=$REMOTE_NAME" \
|
||||
| jq -r '.Reservations[].Instances[].State.Name')
|
||||
if [ -z "$state" ]; then
|
||||
sh_c aws ec2 run-instances \
|
||||
--image-id=ami-0c5300e833c2b32f3 \
|
||||
--count=1 \
|
||||
--instance-type=t3.medium \
|
||||
--security-groups=windows \
|
||||
"--key-name=$KEY_NAME_WINDOWS" \
|
||||
--iam-instance-profile 'Name=AmazonSSMRoleForInstancesQuickSetup' \
|
||||
--block-device-mappings '"DeviceName=/dev/sda1,Ebs={VolumeSize=64,VolumeType=gp3}"' \
|
||||
--tag-specifications "'ResourceType=instance,Tags=[{Key=Name,Value=$REMOTE_NAME}]'" \
|
||||
"'ResourceType=volume,Tags=[{Key=Name,Value=$REMOTE_NAME}]'" >/dev/null
|
||||
fi
|
||||
wait_remote_host_ip
|
||||
log "CI_D2_WINDOWS_AMD64=Administrator@$ip"
|
||||
export CI_D2_WINDOWS_AMD64=Administrator@$ip
|
||||
}
|
||||
|
||||
wait_remote_host_ip() {
|
||||
while true; do
|
||||
ip=$(sh_c aws ec2 describe-instances \
|
||||
--filters 'Name=instance-state-name,Values=pending,running,stopping,stopped' "Name=tag:Name,Values=$REMOTE_NAME" \
|
||||
| jq -r '.Reservations[].Instances[].PublicIpAddress')
|
||||
if [ -n "$ip" ]; then
|
||||
alloc_static_ip
|
||||
ip=$(sh_c aws ec2 describe-instances \
|
||||
--filters 'Name=instance-state-name,Values=pending,running,stopping,stopped' "Name=tag:Name,Values=$REMOTE_NAME" \
|
||||
| jq -r '.Reservations[].Instances[].PublicIpAddress')
|
||||
ssh-keygen -R "$ip"
|
||||
break
|
||||
fi
|
||||
sleep 5
|
||||
done
|
||||
}
|
||||
|
||||
alloc_static_ip() {
|
||||
allocation_id=$(aws ec2 describe-addresses --filters "Name=tag:Name,Values=$REMOTE_NAME" | jq -r '.Addresses[].AllocationId')
|
||||
if [ -z "$allocation_id" ]; then
|
||||
sh_c aws ec2 allocate-address --tag-specifications "'ResourceType=elastic-ip,Tags=[{Key=Name,Value=$REMOTE_NAME}]'"
|
||||
allocation_id=$(aws ec2 describe-addresses --filters "Name=tag:Name,Values=$REMOTE_NAME" | jq -r '.Addresses[].AllocationId')
|
||||
fi
|
||||
|
||||
instance_id=$(aws ec2 describe-instances \
|
||||
--filters 'Name=instance-state-name,Values=pending,running,stopping,stopped' "Name=tag:Name,Values=$REMOTE_NAME" \
|
||||
| jq -r '.Reservations[].Instances[].InstanceId')
|
||||
aws ec2 associate-address --instance-id "$instance_id" --allocation-id "$allocation_id"
|
||||
}
|
||||
|
||||
init_remote_hosts() {
|
||||
bigheader init_remote_hosts
|
||||
|
||||
JOBNAME=$JOBNAME/linux/amd64 runjob_filter REMOTE_HOST=$CI_D2_LINUX_AMD64 REMOTE_NAME=ci-d2-linux-amd64 init_remote_linux
|
||||
JOBNAME=$JOBNAME/linux/arm64 runjob_filter REMOTE_HOST=$CI_D2_LINUX_ARM64 REMOTE_NAME=ci-d2-linux-arm64 init_remote_linux
|
||||
JOBNAME=$JOBNAME/macos/amd64 runjob_filter REMOTE_HOST=$CI_D2_MACOS_AMD64 REMOTE_NAME=ci-d2-macos-amd64 init_remote_macos
|
||||
JOBNAME=$JOBNAME/macos/arm64 runjob_filter REMOTE_HOST=$CI_D2_MACOS_ARM64 REMOTE_NAME=ci-d2-macos-arm64 init_remote_macos
|
||||
JOBNAME=$JOBNAME/windows/amd64 runjob_filter REMOTE_HOST=$CI_D2_WINDOWS_AMD64 REMOTE_NAME=ci-d2-windows-amd64 init_remote_windows
|
||||
|
||||
# Windows and AWS SSM both defeated me.
|
||||
FGCOLOR=3 bigheader "WARNING: WINDOWS INITIALIZATION MUST BE COMPLETED MANUALLY OVER RDP AND POWERSHELL!"
|
||||
}
|
||||
|
||||
init_remote_linux() {
|
||||
header "$REMOTE_NAME"
|
||||
wait_remote_host
|
||||
|
||||
sh_c ssh_copy_id -i="$ID_PUB_PATH" "$REMOTE_HOST"
|
||||
|
||||
sh_c ssh "$REMOTE_HOST" sh -s -- <<EOF
|
||||
set -eux
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
sudo -E apt-get update -y
|
||||
sudo -E apt-get dist-upgrade -y
|
||||
sudo -E apt-get update -y
|
||||
sudo -E apt-get install -y build-essential rsync
|
||||
|
||||
# Docker from https://docs.docker.com/engine/install/ubuntu/
|
||||
sudo -E apt-get -y install \
|
||||
ca-certificates \
|
||||
curl \
|
||||
gnupg \
|
||||
lsb-release
|
||||
sudo mkdir -p /etc/apt/keyrings
|
||||
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --yes --dearmor -o /etc/apt/keyrings/docker.gpg
|
||||
echo \
|
||||
"deb [arch=\$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
|
||||
\$(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||
sudo -E apt-get update -y
|
||||
sudo -E apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
|
||||
sudo groupadd docker || true
|
||||
sudo usermod -aG docker \$USER
|
||||
|
||||
mkdir -p \$HOME/.local/bin
|
||||
mkdir -p \$HOME/.local/share/man
|
||||
EOF
|
||||
init_remote_env
|
||||
|
||||
sh_c ssh "$REMOTE_HOST" sh -s -- <<EOF
|
||||
set -eux
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
sudo -E apt-get autoremove -y
|
||||
EOF
|
||||
sh_c ssh "$REMOTE_HOST" 'sudo reboot' || true
|
||||
}
|
||||
|
||||
init_remote_macos() {
|
||||
header "$REMOTE_NAME"
|
||||
wait_remote_host
|
||||
|
||||
sh_c ssh_copy_id -i="$ID_PUB_PATH" "$REMOTE_HOST"
|
||||
|
||||
sh_c ssh "$REMOTE_HOST" '"/bin/bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\""'
|
||||
|
||||
if sh_c ssh "$REMOTE_HOST" uname -m | grep -qF arm64; then
|
||||
shellenv=$(sh_c ssh "$REMOTE_HOST" /opt/homebrew/bin/brew shellenv)
|
||||
else
|
||||
shellenv=$(sh_c ssh "$REMOTE_HOST" /usr/local/bin/brew shellenv)
|
||||
fi
|
||||
if ! echo "$shellenv" | sh_c ssh "$REMOTE_HOST" "IFS= read -r regex\; \"grep -qF \\\"\\\$regex\\\" ~/.zshrc\""; then
|
||||
echo "$shellenv" | sh_c ssh "$REMOTE_HOST" "\"(echo && cat) >> ~/.zshrc\""
|
||||
fi
|
||||
if ! sh_c ssh "$REMOTE_HOST" "'grep -qF \\\$HOME/.local ~/.zshrc'"; then
|
||||
sh_c ssh "$REMOTE_HOST" "\"(echo && cat) >> ~/.zshrc\"" <<EOF
|
||||
PATH=\$HOME/.local/bin:\$PATH
|
||||
MANPATH=\$HOME/.local/share/man:\$MANPATH
|
||||
EOF
|
||||
fi
|
||||
init_remote_env
|
||||
sh_c ssh "$REMOTE_HOST" brew update
|
||||
sh_c ssh "$REMOTE_HOST" brew upgrade
|
||||
sh_c ssh "$REMOTE_HOST" brew install go rsync
|
||||
|
||||
sh_c ssh "$REMOTE_HOST" 'sudo reboot' || true
|
||||
}
|
||||
|
||||
init_remote_env() {
|
||||
sh_c ssh "$REMOTE_HOST" '"rm -f ~/.ssh/environment"'
|
||||
sh_c ssh "$REMOTE_HOST" '"echo PATH=\$(echo \"echo \\\$PATH\" | \"\$SHELL\" -ils) >\$HOME/.ssh/environment"'
|
||||
sh_c ssh "$REMOTE_HOST" '"echo MANPATH=\$(echo \"echo \\\$MANPATH\" | \"\$SHELL\" -ils) >>\$HOME/.ssh/environment"'
|
||||
|
||||
sh_c ssh "$REMOTE_HOST" "sudo sed -i.bak '\"s/#PermitUserEnvironment no/PermitUserEnvironment yes/\"' /etc/ssh/sshd_config"
|
||||
|
||||
if sh_c ssh "$REMOTE_HOST" uname | grep -qF Darwin; then
|
||||
sh_c ssh "$REMOTE_HOST" "sudo launchctl stop com.openssh.sshd"
|
||||
else
|
||||
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"
|
||||
fi
|
||||
}
|
||||
|
||||
wait_remote_host() {
|
||||
while true; do
|
||||
if sh_c ssh "$REMOTE_HOST" true; then
|
||||
break
|
||||
fi
|
||||
sleep 5
|
||||
done
|
||||
}
|
||||
|
||||
wait_remote_host_windows() {
|
||||
instance_id=$(aws ec2 describe-instances \
|
||||
--filters 'Name=instance-state-name,Values=pending,running,stopping,stopped' "Name=tag:Name,Values=$REMOTE_NAME" \
|
||||
| jq -r '.Reservations[].Instances[].InstanceId')
|
||||
|
||||
while true; do
|
||||
if sh_c aws ssm start-session --target "$instance_id" \
|
||||
--document-name 'AWS-StartNonInteractiveCommand' \
|
||||
--parameters "'{\"command\": [\"echo true\"]}'"; then
|
||||
break
|
||||
fi
|
||||
sleep 5
|
||||
done
|
||||
}
|
||||
|
||||
init_remote_windows() {
|
||||
header "$REMOTE_NAME"
|
||||
wait_remote_host_windows
|
||||
|
||||
init_ps1=$(cat <<EOF
|
||||
\$ProgressPreference = 'SilentlyContinue'
|
||||
|
||||
# Bootstrap PowerShell v7
|
||||
if ((\$PSVersionTable.PSVersion).Major -eq 5) {
|
||||
Invoke-WebRequest -Uri https://www.nuget.org/api/v2/package/Microsoft.UI.Xaml/2.7.3 -OutFile .\microsoft.ui.xaml.2.7.3.zip
|
||||
Expand-Archive -Force .\microsoft.ui.xaml.2.7.3.zip
|
||||
Add-AppxPackage .\microsoft.ui.xaml.2.7.3\tools\AppX\x64\Release\Microsoft.UI.Xaml.2.7.appx
|
||||
|
||||
Invoke-WebRequest -Uri https://github.com/microsoft/winget-cli/releases/download/v1.3.2691/Microsoft.DesktopAppInstaller_8wekyb3d8bbwe.msixbundle -OutFile .\Microsoft.DesktopAppInstaller_8wekyb3d8bbwe.msixbundle
|
||||
Invoke-WebRequest -Uri https://github.com/microsoft/winget-cli/releases/download/v1.3.2691/7bcb1a0ab33340daa57fa5b81faec616_License1.xml -OutFile .\7bcb1a0ab33340daa57fa5b81faec616_License1.xml
|
||||
Invoke-WebRequest -Uri https://aka.ms/Microsoft.VCLibs.x64.14.00.Desktop.appx -OutFile Microsoft.VCLibs.x64.14.00.Desktop.appx
|
||||
Add-AppxProvisionedPackage -online -PackagePath .\Microsoft.DesktopAppInstaller_8wekyb3d8bbwe.msixbundle -LicensePath .\7bcb1a0ab33340daa57fa5b81faec616_License1.xml -DependencyPackagePath Microsoft.VCLibs.x64.14.00.Desktop.appx
|
||||
Add-AppxPackage .\Microsoft.DesktopAppInstaller_8wekyb3d8bbwe.msixbundle
|
||||
|
||||
winget install --silent --accept-package-agreements --accept-source-agreements Microsoft.DotNet.SDK.7
|
||||
# Refresh env.
|
||||
\$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")
|
||||
dotnet tool install --global PowerShell --version 7.3.1
|
||||
pwsh -c 'Enable-ExperimentalFeature PSNativeCommandErrorActionPreference'
|
||||
pwsh .\Desktop\init.ps1
|
||||
Exit
|
||||
}
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
\$ErrorActionPreference = "Stop"
|
||||
\$PSNativeCommandUseErrorActionPreference = \$true
|
||||
|
||||
if (-Not (Get-Command wix -errorAction SilentlyContinue)) {
|
||||
dotnet tool install --global wix --version 4.0.0-preview.1
|
||||
}
|
||||
|
||||
Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0
|
||||
Start-Service sshd
|
||||
Set-Service -Name sshd -StartupType 'Automatic'
|
||||
|
||||
New-ItemProperty -Path "HKLM:\SOFTWARE\OpenSSH" -Name DefaultShell -Value "C:\msys64\usr\bin\bash.exe" -PropertyType String -Force
|
||||
|
||||
ConvertFrom-Json -InputObject @'
|
||||
$(perl -pe 's#\n#\r\n#' "$ID_PUB_PATH" | jq -Rs .)
|
||||
'@ | Out-File -Encoding utf8 "\$env:ProgramData\ssh\administrators_authorized_keys"
|
||||
# utf8BOM -> utf8: https://stackoverflow.com/a/34969243/4283659
|
||||
\$null = New-Item -Force "\$env:ProgramData\ssh\administrators_authorized_keys" -Value (Get-Content -Path "\$env:ProgramData\ssh\administrators_authorized_keys" | Out-String)
|
||||
get-acl "\$env:ProgramData\ssh\ssh_host_rsa_key" | set-acl "\$env:ProgramData\ssh\administrators_authorized_keys"
|
||||
|
||||
if (-Not (Test-Path -Path C:\msys64)) {
|
||||
Invoke-WebRequest -Uri "https://github.com/msys2/msys2-installer/releases/download/2022-10-28/msys2-x86_64-20221028.exe" -OutFile "./msys2-x86_64.exe"
|
||||
./msys2-x86_64.exe install --default-answer --confirm-command --root C:\msys64
|
||||
}
|
||||
C:\msys64\msys2_shell.cmd -defterm -here -no-start -mingw64 -c 'pacman -Sy --noconfirm base-devel vim rsync man'
|
||||
C:\msys64\msys2_shell.cmd -defterm -here -no-start -mingw64 -c 'curl -fsSL https://d2lang.com/install.sh | sh -s -- --tala'
|
||||
C:\msys64\msys2_shell.cmd -defterm -here -no-start -mingw64 -c 'd2 --version'
|
||||
|
||||
\$path = (Get-ItemProperty -Path 'Registry::HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\Environment' -Name Path).Path
|
||||
if (\$path -notlike '*C:\msys64\usr\bin*') {
|
||||
\$path = "\$path;C:\msys64\usr\bin"
|
||||
Set-ItemProperty -Path 'Registry::HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\Environment' -Name Path -Value \$path
|
||||
}
|
||||
if (\$path -notlike '*C:\msys64\usr\local\bin*') {
|
||||
\$path = "\$path;C:\msys64\usr\local\bin"
|
||||
Set-ItemProperty -Path 'Registry::HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\Environment' -Name Path -Value \$path
|
||||
}
|
||||
(Get-ItemProperty -Path 'Registry::HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\Environment' -Name Path).Path
|
||||
|
||||
Restart-Computer
|
||||
EOF
|
||||
|
||||
# To run a POSIX script:
|
||||
# ssh "$CI_D2_WINDOWS_AMD64" sh -s -- <<EOF
|
||||
# wix --version
|
||||
# EOF
|
||||
# To run a command in a pure MSYS2 shell:
|
||||
# ssh "$CI_D2_WINDOWS_AMD64" 'C:\msys64\msys2_shell.cmd -defterm -here -no-start -mingw64 -c "\"d2 --version\""'
|
||||
# To run a pure MSYS2 shell:
|
||||
# ssh -t "$CI_D2_WINDOWS_AMD64" 'C:\msys64\msys2_shell.cmd -defterm -here -no-start -mingw64'
|
||||
|
||||
# In case MSYS2 improves in the future and allows for noninteractive commands the
|
||||
# following will set the OpenSSH shell to MSYS2 instead of PowerShell.
|
||||
#
|
||||
# Right now, setting MSYS2 to the DefaultShell like this will make it start bash in
|
||||
# interactive mode always. Even for ssh "$CI_D2_WINDOWS_AMD64" echo hi. And so you'll end
|
||||
# up with a blank prompt on which to input commands instead of having it execute the
|
||||
# command you passed in via ssh.
|
||||
#
|
||||
# PowerShell as the default is better anyway as it gives us access to both the UNIX
|
||||
# userspace and Windows tools like wix/dotnet/winget.
|
||||
#
|
||||
# To set:
|
||||
# <<EOF
|
||||
# echo '@C:\msys64\msys2_shell.cmd -defterm -here -no-start -mingw64' | Out-File C:\msys64\sshd_default_shell.cmd
|
||||
# # utf8BOM -> utf8: https://stackoverflow.com/a/34969243/4283659
|
||||
# \$null = New-Item -Force C:\msys64\sshd_default_shell.cmd -Value (Get-Content -Path C:\msys64\sshd_default_shell.cmd | Out-String)
|
||||
# Set-ItemProperty -Path HKLM:\SOFTWARE\OpenSSH -Name DefaultShell -Value C:\msys64\sshd_default_shell.cmd
|
||||
# EOF
|
||||
#
|
||||
# To undo:
|
||||
# <<EOF
|
||||
# Remove-ItemProperty -Path HKLM:\SOFTWARE\OpenSSH -Name DefaultShell
|
||||
# rm C:\msys64\sshd_default_shell.cmd
|
||||
# EOF
|
||||
)
|
||||
|
||||
gen_init_ps1=$(cat <<EOF
|
||||
ConvertFrom-Json -InputObject @'
|
||||
$(printf %s "$init_ps1" | perl -pe 'chomp if eof' | perl -pe 's#\n#\r\n#' | jq -Rs .)
|
||||
'@ | Out-File -Encoding utf8 C:\Users\Administrator\Desktop\init.ps1; C:\Users\Administrator\Desktop\init.ps1
|
||||
EOF
|
||||
)
|
||||
|
||||
# Windows and AWS SSM both defeated me.
|
||||
FGCOLOR=3 bigheader "WARNING: WINDOWS INITIALIZATION MUST BE COMPLETED MANUALLY OVER RDP AND POWERSHELL!"
|
||||
|
||||
warn '1. Obtain Windows RDP password with:'
|
||||
echo " aws ec2 get-password-data --instance-id \$(aws ec2 describe-instances --filters 'Name=instance-state-name,Values=pending,running,stopping,stopped' "Name=tag:Name,Values=$REMOTE_NAME" | jq -r '.Reservations[].Instances[].InstanceId') --priv-launch-key windows.pem | jq -r .PasswordData" >&2
|
||||
warn "2. RDP into $REMOTE_HOST and open PowerShell."
|
||||
warn '3. Generate and execute C:\Users\Administrator\Desktop\init.ps1 with:'
|
||||
printf '%s\n' "$gen_init_ps1" >&2
|
||||
warn '4. Run the following to be notified once installation is successful:'
|
||||
cat <<EOF
|
||||
until ssh $REMOTE_HOST d2 --version; do echo 'failed: retrying in 5s' && sleep 5; done && printf 'success\a\n'
|
||||
EOF
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
#!/bin/sh
|
||||
set -eu
|
||||
. "$(dirname "$0")/../../../ci/sub/lib.sh"
|
||||
cd -- "$(dirname "$0")/../../.."
|
||||
. ./ci/sub/lib.sh
|
||||
|
||||
help() {
|
||||
cat <<EOF
|
||||
|
|
@ -33,10 +33,11 @@ main() {
|
|||
done
|
||||
shift "$FLAGSHIFT"
|
||||
|
||||
REMOTE_HOST=$TSTRUCT_LINUX_AMD64_BUILDER; runjob linux-amd64 ssh "$REMOTE_HOST" "$@"
|
||||
REMOTE_HOST=$TSTRUCT_LINUX_ARM64_BUILDER; runjob linux-arm64 ssh "$REMOTE_HOST" "$@"
|
||||
REMOTE_HOST=$TSTRUCT_MACOS_AMD64_BUILDER; runjob macos-amd64 ssh "$REMOTE_HOST" "$@"
|
||||
REMOTE_HOST=$TSTRUCT_MACOS_ARM64_BUILDER; runjob macos-arm64 ssh "$REMOTE_HOST" "$@"
|
||||
REMOTE_HOST=$CI_HOST_D2_LINUX_AMD64 && runjob linux-amd64 ssh "$REMOTE_HOST" "$@"
|
||||
REMOTE_HOST=$CI_HOST_D2_LINUX_ARM64 && runjob linux-arm64 ssh "$REMOTE_HOST" "$@"
|
||||
REMOTE_HOST=$CI_HOST_D2_MACOS_AMD64 && runjob macos-amd64 ssh "$REMOTE_HOST" "$@"
|
||||
REMOTE_HOST=$CI_HOST_D2_MACOS_ARM64 && runjob macos-arm64 ssh "$REMOTE_HOST" "$@"
|
||||
REMOTE_HOST=$CI_HOST_D2_WINDOWS_AMD64 && runjob macos-arm64 ssh "$REMOTE_HOST" "$@"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
|
@ -1,7 +1,12 @@
|
|||
#!/bin/sh
|
||||
set -eu
|
||||
if [ ! -e "$(dirname "$0")/../../ci/sub/.git" ]; then
|
||||
set -x
|
||||
git submodule update --init
|
||||
set +x
|
||||
fi
|
||||
. "$(dirname "$0")/../../ci/sub/lib.sh"
|
||||
cd -- "$(dirname "$0")/../.."
|
||||
. ./ci/sub/lib.sh
|
||||
|
||||
help() {
|
||||
cat <<EOF
|
||||
|
|
@ -20,11 +25,12 @@ Flags:
|
|||
changed something and need to force rebuild, use this flag.
|
||||
|
||||
--local
|
||||
By default build.sh uses \$TSTRUCT_MACOS_AMD64_BUILDER, \$TSTRUCT_MACOS_ARM64_BUILDER,
|
||||
\$TSTRUCT_LINUX_AMD64_BUILDER and \$TSTRUCT_LINUX_ARM64_BUILDER to build the release
|
||||
By default build.sh uses \$CI_D2_LINUX_AMD64, \$CI_D2_LINUX_ARM64,
|
||||
\$CI_D2_MACOS_AMD64 and \$CI_D2_MACOS_ARM64 to build the release
|
||||
archives. It's required for now due to the following issue:
|
||||
https://github.com/terrastruct/d2/issues/31 With --local, build.sh will cross compile
|
||||
locally. warning: This is only for testing purposes, do not use in production!
|
||||
https://github.com/terrastruct/d2/issues/31
|
||||
With --local, build.sh will cross compile locally. warning: This is only for testing
|
||||
purposes, do not use in production!
|
||||
|
||||
--host-only
|
||||
Use to build the release archive for the host OS-ARCH only. All logging is done to stderr
|
||||
|
|
@ -45,6 +51,13 @@ Flags:
|
|||
|
||||
--uninstall
|
||||
Ensure a release using --host-only and uninstall it.
|
||||
|
||||
--push-docker
|
||||
Push the built docker image. Unfortunately dockerx requires the multi-arch images be
|
||||
pushed if required in the same invocation as build. dockerx cannot load multi-arch
|
||||
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
|
||||
EOF
|
||||
}
|
||||
|
||||
|
|
@ -69,7 +82,7 @@ main() {
|
|||
;;
|
||||
run)
|
||||
flag_reqarg && shift "$FLAGSHIFT"
|
||||
JOBFILTER="$FLAGARG"
|
||||
JOBFILTER=$FLAGARG
|
||||
;;
|
||||
host-only)
|
||||
flag_noarg && shift "$FLAGSHIFT"
|
||||
|
|
@ -96,6 +109,10 @@ main() {
|
|||
HOST_ONLY=1
|
||||
LOCAL=1
|
||||
;;
|
||||
push-docker)
|
||||
flag_noarg && shift "$FLAGSHIFT"
|
||||
PUSH_DOCKER=1
|
||||
;;
|
||||
*)
|
||||
flag_errusage "unrecognized flag $FLAGRAW"
|
||||
;;
|
||||
|
|
@ -108,10 +125,13 @@ main() {
|
|||
|
||||
VERSION=${VERSION:-$(git_describe_ref)}
|
||||
BUILD_DIR=ci/release/build/$VERSION
|
||||
sh_c mkdir -p "$BUILD_DIR"
|
||||
sh_c rm -f ci/release/build/latest
|
||||
sh_c ln -s "$VERSION" ci/release/build/latest
|
||||
if [ -n "${HOST_ONLY-}" ]; then
|
||||
ensure_os
|
||||
ensure_arch
|
||||
runjob "$OS-$ARCH" "build"
|
||||
runjob "$OS/$ARCH" "build"
|
||||
|
||||
if [ -n "${INSTALL-}" ]; then
|
||||
sh_c make -sC "ci/release/build/$VERSION/$OS-$ARCH/d2-$VERSION" install
|
||||
|
|
@ -121,12 +141,16 @@ main() {
|
|||
return 0
|
||||
fi
|
||||
|
||||
runjob linux-amd64 'OS=linux ARCH=amd64 build' &
|
||||
runjob linux-arm64 'OS=linux ARCH=arm64 build' &
|
||||
runjob macos-amd64 'OS=macos ARCH=amd64 build' &
|
||||
runjob macos-arm64 'OS=macos ARCH=arm64 build' &
|
||||
runjob windows-amd64 'OS=windows ARCH=amd64 build' &
|
||||
runjob windows-arm64 'OS=windows ARCH=arm64 build' &
|
||||
runjob linux/amd64 'OS=linux ARCH=amd64 build' &
|
||||
runjob linux/arm64 'OS=linux ARCH=arm64 build' &
|
||||
runjob macos/amd64 'OS=macos ARCH=amd64 build' &
|
||||
runjob macos/arm64 'OS=macos ARCH=arm64 build' &
|
||||
runjob windows/amd64 'OS=windows ARCH=amd64 build' &
|
||||
runjob windows/arm64 'OS=windows ARCH=arm64 build' &
|
||||
waitjobs
|
||||
|
||||
runjob linux/dockerimage 'OS=linux build_docker_image' &
|
||||
runjob windows/amd64/msi 'OS=windows ARCH=amd64 build_windows_msi' &
|
||||
waitjobs
|
||||
}
|
||||
|
||||
|
|
@ -148,10 +172,10 @@ build() {
|
|||
macos)
|
||||
case $ARCH in
|
||||
amd64)
|
||||
REMOTE_HOST=$TSTRUCT_MACOS_AMD64_BUILDER build_remote_macos
|
||||
REMOTE_HOST=$CI_D2_MACOS_AMD64 build_remote_macos
|
||||
;;
|
||||
arm64)
|
||||
REMOTE_HOST=$TSTRUCT_MACOS_ARM64_BUILDER build_remote_macos
|
||||
REMOTE_HOST=$CI_D2_MACOS_ARM64 build_remote_macos
|
||||
;;
|
||||
*)
|
||||
warn "no builder for OS=$OS ARCH=$ARCH, building locally..."
|
||||
|
|
@ -162,10 +186,10 @@ build() {
|
|||
linux)
|
||||
case $ARCH in
|
||||
amd64)
|
||||
REMOTE_HOST=$TSTRUCT_LINUX_AMD64_BUILDER build_remote_linux
|
||||
REMOTE_HOST=$CI_D2_LINUX_AMD64 build_remote_linux
|
||||
;;
|
||||
arm64)
|
||||
REMOTE_HOST=$TSTRUCT_LINUX_ARM64_BUILDER build_remote_linux
|
||||
REMOTE_HOST=$CI_D2_LINUX_ARM64 build_remote_linux
|
||||
;;
|
||||
*)
|
||||
warn "no builder for OS=$OS ARCH=$ARCH, building locally..."
|
||||
|
|
@ -190,7 +214,7 @@ build_local() {
|
|||
sh_c ./ci/release/_build.sh
|
||||
}
|
||||
|
||||
build_remote_macos() {
|
||||
build_remote_macos() {(
|
||||
sh_c lockfile_ssh "$REMOTE_HOST" .d2-build-lock
|
||||
sh_c gitsync "$REMOTE_HOST" src/d2
|
||||
sh_c ssh "$REMOTE_HOST" "COLOR=${COLOR-} \
|
||||
|
|
@ -205,9 +229,9 @@ PATH=\\\"/usr/local/bin:/usr/local/sbin:/opt/homebrew/bin:/opt/homebrew/sbin\\\$
|
|||
./src/d2/ci/release/_build.sh"
|
||||
sh_c mkdir -p "$HW_BUILD_DIR"
|
||||
sh_c rsync --archive --human-readable "$REMOTE_HOST:src/d2/$ARCHIVE" "$ARCHIVE"
|
||||
}
|
||||
)}
|
||||
|
||||
build_remote_linux() {
|
||||
build_remote_linux() {(
|
||||
sh_c lockfile_ssh "$REMOTE_HOST" .d2-build-lock
|
||||
sh_c gitsync "$REMOTE_HOST" src/d2
|
||||
sh_c ssh "$REMOTE_HOST" "COLOR=${COLOR-} \
|
||||
|
|
@ -218,13 +242,39 @@ VERSION=$VERSION \
|
|||
OS=$OS \
|
||||
ARCH=$ARCH \
|
||||
ARCHIVE=$ARCHIVE \
|
||||
./src/d2/ci/release/build_docker.sh"
|
||||
./src/d2/ci/release/build_in_docker.sh"
|
||||
sh_c mkdir -p "$HW_BUILD_DIR"
|
||||
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'
|
||||
fi
|
||||
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"
|
||||
}
|
||||
|
||||
ssh() {
|
||||
command ssh -o='StrictHostKeyChecking=accept-new' "$@"
|
||||
build_windows_msi() {
|
||||
REMOTE_HOST=$CI_D2_WINDOWS_AMD64
|
||||
|
||||
ln -sf "../build/$VERSION/windows-amd64/d2-$VERSION/bin/d2.exe" ./ci/release/windows/d2.exe
|
||||
sh_c rsync --archive --human-readable --copy-links --delete ./ci/release/windows/ "'$REMOTE_HOST:windows/'"
|
||||
if ! echo "$VERSION" | grep '[0-9]\.[0-9]\.[0-9]'; then
|
||||
WIX_VERSION=0.0.0
|
||||
else
|
||||
WIX_VERSION=$VERSION
|
||||
fi
|
||||
sh_c ssh "$REMOTE_HOST" "'cd ./windows && wix build -arch x64 -d D2Version=$WIX_VERSION ./d2.wxs'"
|
||||
|
||||
# --files-from shouldn't be necessary but for some reason selecting d2.msi directly
|
||||
# makes rsync error with:
|
||||
# ERROR: rejecting unrequested file-list name: ./windows/d2.msi
|
||||
# rsync error: requested action not supported (code 4) at flist.c(1027) [Receiver=3.2.7]
|
||||
rsync_files=$(mktempd)/rsync-files
|
||||
echo d2.msi >$rsync_files
|
||||
sh_c rsync --archive --human-readable --files-from "$rsync_files" "'$REMOTE_HOST:windows/'" "./ci/release/build/$VERSION/d2-$VERSION-$OS-$ARCH.msi"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
|
|
|||
1
ci/release/build/.dockerignore
Normal file
|
|
@ -0,0 +1 @@
|
|||
**/d2*/
|
||||
|
|
@ -5,7 +5,7 @@ cd -- "$(dirname "$0")/../.."
|
|||
|
||||
tag="$(sh_c docker build \
|
||||
--build-arg GOVERSION="1.19.3.linux-$ARCH" \
|
||||
-qf ./ci/release/builders/Dockerfile ./ci/release/builders)"
|
||||
-qf ./ci/release/linux/Dockerfile ./ci/release/linux)"
|
||||
docker_run \
|
||||
-e DRY_RUN \
|
||||
-e HW_BUILD_DIR \
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
FROM centos:7
|
||||
|
||||
ARG GOVERSION=
|
||||
|
||||
RUN curl -fsSL "https://go.dev/dl/go$GOVERSION.tar.gz" >/tmp/go.tar.gz
|
||||
RUN tar -C /usr/local -xzf /tmp/go.tar.gz
|
||||
|
||||
ENV PATH="/usr/local/go/bin:$PATH"
|
||||
|
||||
RUN yum install -y rsync wget
|
||||
RUN yum groupinstall -y 'Development Tools'
|
||||
|
||||
RUN curl -fsSL https://ftp.gnu.org/gnu/gcc/gcc-5.2.0/gcc-5.2.0.tar.gz >/tmp/gcc.tar.gz
|
||||
RUN tar -C /usr/local -xzf /tmp/gcc.tar.gz
|
||||
RUN cd /usr/local/gcc-5.2.0 && ./contrib/download_prerequisites && mkdir -p build \
|
||||
&& cd build && ../configure --disable-multilib && make && make install
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
#!/bin/sh
|
||||
set -eu
|
||||
cd -- "$(dirname "$0")/../../.."
|
||||
. ./ci/sub/lib.sh
|
||||
|
||||
help() {
|
||||
cat <<EOF
|
||||
usage: $0 [--dry-run] -i keys.pub
|
||||
|
||||
$0 copies keys.pub to each builder and then deduplicates its .authorized_keys.
|
||||
EOF
|
||||
}
|
||||
|
||||
main() {
|
||||
while flag_parse "$@"; do
|
||||
case "$FLAG" in
|
||||
h|help)
|
||||
help
|
||||
return 0
|
||||
;;
|
||||
dry-run)
|
||||
flag_noarg && shift "$FLAGSHIFT"
|
||||
DRY_RUN=1
|
||||
;;
|
||||
i)
|
||||
flag_nonemptyarg && shift "$FLAGSHIFT"
|
||||
KEY_FILE=$FLAGARG
|
||||
;;
|
||||
*)
|
||||
flag_errusage "unrecognized flag $FLAGRAW"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
shift "$FLAGSHIFT"
|
||||
if [ -z "${KEY_FILE-}" ]; then
|
||||
echoerr "-i is required"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
header linux-amd64
|
||||
REMOTE_HOST=$TSTRUCT_LINUX_AMD64_BUILDER copy_keys
|
||||
header linux-arm64
|
||||
REMOTE_HOST=$TSTRUCT_LINUX_ARM64_BUILDER copy_keys
|
||||
header macos-amd64
|
||||
REMOTE_HOST=$TSTRUCT_MACOS_AMD64_BUILDER copy_keys
|
||||
header macos-arm64
|
||||
REMOTE_HOST=$TSTRUCT_MACOS_ARM64_BUILDER copy_keys
|
||||
}
|
||||
|
||||
copy_keys() {
|
||||
sh_c ssh-copy-id -fi "$KEY_FILE" "$REMOTE_HOST"
|
||||
sh_c ssh "$REMOTE_HOST" 'cat .ssh/authorized_keys \| sort -u \> .ssh/authorized_keys.dedup'
|
||||
sh_c ssh "$REMOTE_HOST" 'cp .ssh/authorized_keys.dedup .ssh/authorized_keys'
|
||||
sh_c ssh "$REMOTE_HOST" 'rm .ssh/authorized_keys.dedup'
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
|
@ -1,294 +0,0 @@
|
|||
#!/bin/sh
|
||||
set -eu
|
||||
cd -- "$(dirname "$0")/../../.."
|
||||
. ./ci/sub/lib.sh
|
||||
|
||||
help() {
|
||||
cat <<EOF
|
||||
usage: $0 [--dry-run] [--skip-create]
|
||||
|
||||
$0 creates and ensures the d2 builders in AWS.
|
||||
EOF
|
||||
}
|
||||
|
||||
main() {
|
||||
while flag_parse "$@"; do
|
||||
case "$FLAG" in
|
||||
h|help)
|
||||
help
|
||||
return 0
|
||||
;;
|
||||
dry-run)
|
||||
flag_noarg && shift "$FLAGSHIFT"
|
||||
DRY_RUN=1
|
||||
;;
|
||||
skip-create)
|
||||
flag_noarg && shift "$FLAGSHIFT"
|
||||
SKIP_CREATE=1
|
||||
;;
|
||||
*)
|
||||
flag_errusage "unrecognized flag $FLAGRAW"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
shift "$FLAGSHIFT"
|
||||
if [ $# -gt 0 ]; then
|
||||
flag_errusage "no arguments are accepted"
|
||||
fi
|
||||
|
||||
if [ -z "${SKIP_CREATE-}" ]; then
|
||||
create_remote_hosts
|
||||
fi
|
||||
init_remote_hosts
|
||||
}
|
||||
|
||||
create_remote_hosts() {
|
||||
KEY_NAME=$(aws ec2 describe-key-pairs | jq -r .KeyPairs[0].KeyName)
|
||||
VPC_ID=$(aws ec2 describe-vpcs | jq -r .Vpcs[0].VpcId)
|
||||
|
||||
header security-group
|
||||
SG_ID=$(aws ec2 describe-security-groups --group-names ssh 2>/dev/null \
|
||||
| jq -r .SecurityGroups[0].GroupId)
|
||||
if [ -z "$SG_ID" ]; then
|
||||
SG_ID=$(sh_c aws ec2 create-security-group \
|
||||
--group-name ssh \
|
||||
--description ssh \
|
||||
--vpc-id "$VPC_ID" | jq -r .GroupId)
|
||||
fi
|
||||
|
||||
header security-group-ingress
|
||||
SG_RULES_COUNT=$(aws ec2 describe-security-groups --group-names ssh \
|
||||
| jq -r '.SecurityGroups[0].IpPermissions | length')
|
||||
if [ "$SG_RULES_COUNT" -eq 0 ]; then
|
||||
sh_c aws ec2 authorize-security-group-ingress \
|
||||
--group-id "$SG_ID" \
|
||||
--protocol tcp \
|
||||
--port 22 \
|
||||
--cidr 0.0.0.0/0 >/dev/null
|
||||
fi
|
||||
|
||||
header linux-amd64
|
||||
state=$(aws ec2 describe-instances --filters \
|
||||
'Name=instance-state-name,Values=pending,running,stopping,stopped' 'Name=tag:Name,Values=d2-builder-linux-amd64' \
|
||||
| jq -r '.Reservations[].Instances[].State.Name')
|
||||
if [ -z "$state" ]; then
|
||||
sh_c aws ec2 run-instances \
|
||||
--image-id=ami-071e6cafc48327ca2 \
|
||||
--count=1 \
|
||||
--instance-type=t2.small \
|
||||
--security-groups=ssh \
|
||||
"--key-name=$KEY_NAME" \
|
||||
--tag-specifications '"ResourceType=instance,Tags=[{Key=Name,Value=d2-builder-linux-amd64}]"' \
|
||||
'"ResourceType=volume,Tags=[{Key=Name,Value=d2-builder-linux-amd64}]"' >/dev/null
|
||||
fi
|
||||
while true; do
|
||||
dnsname=$(sh_c aws ec2 describe-instances \
|
||||
--filters 'Name=instance-state-name,Values=pending,running,stopping,stopped' 'Name=tag:Name,Values=d2-builder-linux-amd64' \
|
||||
| jq -r '.Reservations[].Instances[].PublicDnsName')
|
||||
if [ -n "$dnsname" ]; then
|
||||
log "TSTRUCT_LINUX_AMD64_BUILDER=admin@$dnsname"
|
||||
export TSTRUCT_LINUX_AMD64_BUILDER=admin@$dnsname
|
||||
break
|
||||
fi
|
||||
sleep 5
|
||||
done
|
||||
|
||||
header linux-arm64
|
||||
state=$(aws ec2 describe-instances --filters \
|
||||
'Name=instance-state-name,Values=pending,running,stopping,stopped' 'Name=tag:Name,Values=d2-builder-linux-arm64' \
|
||||
| jq -r '.Reservations[].Instances[].State.Name')
|
||||
if [ -z "$state" ]; then
|
||||
sh_c aws ec2 run-instances \
|
||||
--image-id=ami-0e67506f183e5ab60 \
|
||||
--count=1 \
|
||||
--instance-type=t4g.small \
|
||||
--security-groups=ssh \
|
||||
"--key-name=$KEY_NAME" \
|
||||
--tag-specifications '"ResourceType=instance,Tags=[{Key=Name,Value=d2-builder-linux-arm64}]"' \
|
||||
'"ResourceType=volume,Tags=[{Key=Name,Value=d2-builder-linux-arm64}]"' >/dev/null
|
||||
fi
|
||||
while true; do
|
||||
dnsname=$(sh_c aws ec2 describe-instances \
|
||||
--filters 'Name=instance-state-name,Values=pending,running,stopping,stopped' 'Name=tag:Name,Values=d2-builder-linux-arm64' \
|
||||
| jq -r '.Reservations[].Instances[].PublicDnsName')
|
||||
if [ -n "$dnsname" ]; then
|
||||
log "TSTRUCT_LINUX_ARM64_BUILDER=admin@$dnsname"
|
||||
export TSTRUCT_LINUX_ARM64_BUILDER=admin@$dnsname
|
||||
break
|
||||
fi
|
||||
sleep 5
|
||||
done
|
||||
|
||||
header "macos-amd64-host"
|
||||
MACOS_AMD64_HOST_ID=$(aws ec2 describe-hosts --filter 'Name=state,Values=pending,available' 'Name=tag:Name,Values=d2-builder-macos-amd64' | jq -r '.Hosts[].HostId')
|
||||
if [ -z "$MACOS_AMD64_HOST_ID" ]; then
|
||||
MACOS_AMD64_HOST_ID=$(sh_c aws ec2 allocate-hosts --instance-type mac1.metal --quantity 1 --availability-zone us-west-2a \
|
||||
--tag-specifications '"ResourceType=dedicated-host,Tags=[{Key=Name,Value=d2-builder-macos-amd64}]"' \
|
||||
| jq -r .HostIds[0])
|
||||
fi
|
||||
|
||||
header "macos-arm64-host"
|
||||
MACOS_ARM64_HOST_ID=$(aws ec2 describe-hosts --filter 'Name=state,Values=pending,available' 'Name=tag:Name,Values=d2-builder-macos-arm64' | jq -r '.Hosts[].HostId')
|
||||
if [ -z "$MACOS_ARM64_HOST_ID" ]; then
|
||||
MACOS_ARM64_HOST_ID=$(sh_c aws ec2 allocate-hosts --instance-type mac2.metal --quantity 1 --availability-zone us-west-2a \
|
||||
--tag-specifications '"ResourceType=dedicated-host,Tags=[{Key=Name,Value=d2-builder-macos-amd64}]"' \
|
||||
| jq -r .HostIds[0])
|
||||
fi
|
||||
|
||||
header macos-amd64
|
||||
state=$(aws ec2 describe-instances --filters \
|
||||
'Name=instance-state-name,Values=pending,running,stopping,stopped' 'Name=tag:Name,Values=d2-builder-macos-amd64' \
|
||||
| jq -r '.Reservations[].Instances[].State.Name')
|
||||
if [ -z "$state" ]; then
|
||||
sh_c aws ec2 run-instances \
|
||||
--image-id=ami-0dd2ded7568750663 \
|
||||
--count=1 \
|
||||
--instance-type=mac1.metal \
|
||||
--security-groups=ssh \
|
||||
"--key-name=$KEY_NAME" \
|
||||
--placement "Tenancy=host,HostId=$MACOS_AMD64_HOST_ID" \
|
||||
--tag-specifications '"ResourceType=instance,Tags=[{Key=Name,Value=d2-builder-macos-amd64}]"' \
|
||||
'"ResourceType=volume,Tags=[{Key=Name,Value=d2-builder-macos-amd64}]"' >/dev/null
|
||||
fi
|
||||
while true; do
|
||||
dnsname=$(sh_c aws ec2 describe-instances \
|
||||
--filters 'Name=instance-state-name,Values=pending,running,stopping,stopped' 'Name=tag:Name,Values=d2-builder-macos-amd64' \
|
||||
| jq -r '.Reservations[].Instances[].PublicDnsName')
|
||||
if [ -n "$dnsname" ]; then
|
||||
log "TSTRUCT_MACOS_AMD64_BUILDER=ec2-user@$dnsname"
|
||||
export TSTRUCT_MACOS_AMD64_BUILDER=ec2-user@$dnsname
|
||||
break
|
||||
fi
|
||||
sleep 5
|
||||
done
|
||||
|
||||
header macos-arm64
|
||||
state=$(aws ec2 describe-instances --filters \
|
||||
'Name=instance-state-name,Values=pending,running,stopping,stopped' 'Name=tag:Name,Values=d2-builder-macos-arm64' \
|
||||
| jq -r '.Reservations[].Instances[].State.Name')
|
||||
if [ -z "$state" ]; then
|
||||
sh_c aws ec2 run-instances \
|
||||
--image-id=ami-0af0516ff2c43dbbe \
|
||||
--count=1 \
|
||||
--instance-type=mac2.metal \
|
||||
--security-groups=ssh \
|
||||
"--key-name=$KEY_NAME" \
|
||||
--placement "Tenancy=host,HostId=$MACOS_ARM64_HOST_ID" \
|
||||
--tag-specifications '"ResourceType=instance,Tags=[{Key=Name,Value=d2-builder-macos-arm64}]"' \
|
||||
'"ResourceType=volume,Tags=[{Key=Name,Value=d2-builder-macos-arm64}]"' >/dev/null
|
||||
fi
|
||||
while true; do
|
||||
dnsname=$(sh_c aws ec2 describe-instances \
|
||||
--filters 'Name=instance-state-name,Values=pending,running,stopping,stopped' 'Name=tag:Name,Values=d2-builder-macos-arm64' \
|
||||
| jq -r '.Reservations[].Instances[].PublicDnsName')
|
||||
if [ -n "$dnsname" ]; then
|
||||
log "TSTRUCT_MACOS_ARM64_BUILDER=ec2-user@$dnsname"
|
||||
export TSTRUCT_MACOS_ARM64_BUILDER=ec2-user@$dnsname
|
||||
break
|
||||
fi
|
||||
sleep 5
|
||||
done
|
||||
}
|
||||
|
||||
init_remote_hosts() {
|
||||
header linux-amd64
|
||||
REMOTE_HOST=$TSTRUCT_LINUX_AMD64_BUILDER init_remote_linux
|
||||
header linux-arm64
|
||||
REMOTE_HOST=$TSTRUCT_LINUX_ARM64_BUILDER init_remote_linux
|
||||
header macos-amd64
|
||||
REMOTE_HOST=$TSTRUCT_MACOS_AMD64_BUILDER init_remote_macos
|
||||
header macos-arm64
|
||||
REMOTE_HOST=$TSTRUCT_MACOS_ARM64_BUILDER init_remote_macos
|
||||
|
||||
FGCOLOR=2 header summary
|
||||
log "export TSTRUCT_LINUX_AMD64_BUILDER=$TSTRUCT_LINUX_AMD64_BUILDER"
|
||||
log "export TSTRUCT_LINUX_ARM64_BUILDER=$TSTRUCT_LINUX_ARM64_BUILDER"
|
||||
log "export TSTRUCT_MACOS_AMD64_BUILDER=$TSTRUCT_MACOS_AMD64_BUILDER"
|
||||
log "export TSTRUCT_MACOS_ARM64_BUILDER=$TSTRUCT_MACOS_ARM64_BUILDER"
|
||||
}
|
||||
|
||||
init_remote_linux() {
|
||||
wait_remote_host
|
||||
|
||||
sh_c ssh "$REMOTE_HOST" sh -s -- <<EOF
|
||||
set -eux
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
sudo -E apt-get update -y
|
||||
sudo -E apt-get dist-upgrade -y
|
||||
sudo -E apt-get install -y build-essential rsync
|
||||
|
||||
# Docker from https://docs.docker.com/engine/install/debian/
|
||||
sudo -E apt-get -y install \
|
||||
ca-certificates \
|
||||
curl \
|
||||
gnupg \
|
||||
lsb-release
|
||||
sudo mkdir -p /etc/apt/keyrings
|
||||
curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --batch --yes --dearmor -o /etc/apt/keyrings/docker.gpg
|
||||
echo \
|
||||
"deb [arch=\$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian \
|
||||
\$(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||
sudo -E apt-get update -y
|
||||
sudo -E apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
|
||||
sudo groupadd docker || true
|
||||
sudo usermod -aG docker \$USER
|
||||
|
||||
mkdir -p \$HOME/.local/bin
|
||||
mkdir -p \$HOME/.local/share/man
|
||||
EOF
|
||||
init_remote_env
|
||||
|
||||
sh_c ssh "$REMOTE_HOST" 'sudo reboot' || true
|
||||
}
|
||||
|
||||
init_remote_macos() {
|
||||
wait_remote_host
|
||||
|
||||
sh_c ssh "$REMOTE_HOST" '"/bin/bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\""'
|
||||
|
||||
if sh_c ssh "$REMOTE_HOST" uname -m | grep -qF arm64; then
|
||||
shellenv=$(sh_c ssh "$REMOTE_HOST" /opt/homebrew/bin/brew shellenv)
|
||||
else
|
||||
shellenv=$(sh_c ssh "$REMOTE_HOST" /usr/local/bin/brew shellenv)
|
||||
fi
|
||||
if ! echo "$shellenv" | sh_c ssh "$REMOTE_HOST" "IFS= read -r regex\; \"grep -qF \\\"\\\$regex\\\" ~/.zshrc\""; then
|
||||
echo "$shellenv" | sh_c ssh "$REMOTE_HOST" "\"(echo && cat) >> ~/.zshrc\""
|
||||
fi
|
||||
if ! sh_c ssh "$REMOTE_HOST" "'grep -qF \\\$HOME/.local ~/.zshrc'"; then
|
||||
sh_c ssh "$REMOTE_HOST" "\"(echo && cat) >> ~/.zshrc\"" <<EOF
|
||||
PATH=\$HOME/.local/bin:\$PATH
|
||||
MANPATH=\$HOME/.local/share/man:\$MANPATH
|
||||
EOF
|
||||
fi
|
||||
|
||||
init_remote_env
|
||||
sh_c ssh "$REMOTE_HOST" brew update
|
||||
sh_c ssh "$REMOTE_HOST" brew upgrade
|
||||
sh_c ssh "$REMOTE_HOST" brew install go rsync
|
||||
}
|
||||
|
||||
init_remote_env() {
|
||||
sh_c ssh "$REMOTE_HOST" '"rm -f ~/.ssh/environment"'
|
||||
sh_c ssh "$REMOTE_HOST" '"echo PATH=\$(echo \"echo \\\$PATH\" | \"\$SHELL\" -ils) >\$HOME/.ssh/environment"'
|
||||
sh_c ssh "$REMOTE_HOST" '"echo MANPATH=\$(echo \"echo \\\$MANPATH\" | \"\$SHELL\" -ils) >>\$HOME/.ssh/environment"'
|
||||
|
||||
sh_c ssh "$REMOTE_HOST" "sudo sed -i.bak '\"s/#PermitUserEnvironment no/PermitUserEnvironment yes/\"' /etc/ssh/sshd_config"
|
||||
|
||||
if sh_c ssh "$REMOTE_HOST" uname | grep -qF Darwin; then
|
||||
sh_c ssh "$REMOTE_HOST" "sudo launchctl stop com.openssh.sshd"
|
||||
else
|
||||
sh_c ssh "$REMOTE_HOST" "sudo systemctl restart sshd"
|
||||
fi
|
||||
}
|
||||
|
||||
wait_remote_host() {
|
||||
while true; do
|
||||
if sh_c ssh "$REMOTE_HOST" true; then
|
||||
break
|
||||
fi
|
||||
sleep 5
|
||||
done
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
|
@ -1,7 +1,16 @@
|
|||
#### Features 🚀
|
||||
|
||||
- Tooltips can be set on shapes. See [https://d2lang.com/tour/tooltips](https://d2lang.com/tour/interactive). [#548](https://github.com/terrastruct/d2/pull/548)
|
||||
- Links can be set on shapes. See [https://d2lang.com/tour/tooltips](https://d2lang.com/tour/interactive). [#548](https://github.com/terrastruct/d2/pull/548)
|
||||
- The `width` and `height` attributes are no longer restricted to images and can be applied to non-container shapes. [#498](https://github.com/terrastruct/d2/pull/498)
|
||||
|
||||
#### Improvements 🧹
|
||||
|
||||
- Watch mode now renders fit to screen. [#560](https://github.com/terrastruct/d2/pull/560)
|
||||
|
||||
#### Bugfixes ⛑️
|
||||
|
||||
- Fixed sequence diagram span size for self-edges [#397](https://github.com/terrastruct/d2/pull/397)
|
||||
- Restricts where `near` key constant values can be used, with good error messages, instead of erroring (e.g. setting `near: top-center` on a container would cause bad layouts or error). [#538](https://github.com/terrastruct/d2/pull/538)
|
||||
- Fixes an error during ELK layout when images had empty labels. [#555](https://github.com/terrastruct/d2/pull/555)
|
||||
- Fixes rendering classes and tables with empty headers. [#498](https://github.com/terrastruct/d2/pull/498)
|
||||
- Fixes rendering sql tables with no columns. [#553](https://github.com/terrastruct/d2/pull/553)
|
||||
|
|
|
|||
7
ci/release/changelogs/v0.1.1.md
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
#### Improvements 🧹
|
||||
|
||||
- The Windows release binary is now suffixed correctly with `.exe` [#388](https://github.com/terrastruct/d2/issues/388)
|
||||
|
||||
#### Bugfixes ⛑️
|
||||
|
||||
- Fixed sequence diagram span size for self-edges [#397](https://github.com/terrastruct/d2/pull/397)
|
||||
25
ci/release/changelogs/v0.1.2.md
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
D2 now has an official playground site: [https://play.d2lang.com](https://play.d2lang.com). It loads and runs fast, works on all the major browsers and has been tested on desktop and mobile on a variety of devices. It's the easiest way to get started with D2 and share diagrams. The playground is all open source ([https://github.com/terrastruct/d2-playground](https://github.com/terrastruct/d2-playground)). We'd love to hear your feedback and feature requests.
|
||||
|
||||
Windows users, the install experience just got a whole lot better. Making D2 accessible and easy to use continues to be a priority for us. With this release, we added an MSI installer for Windows, so that installs are just a few clicks. An official Docker image has also been added.
|
||||
|
||||
#### Features 🚀
|
||||
|
||||
- Diagram padding can be configured in the CLI (default 100px). [https://github.com/terrastruct/d2/pull/431](https://github.com/terrastruct/d2/pull/431)
|
||||
- Connection label backgrounds can be set with the `style.fill` keyword. [https://github.com/terrastruct/d2/pull/452](https://github.com/terrastruct/d2/pull/452)
|
||||
- Adds official Docker image. See [./docs/INSTALL.md#docker](./docs/INSTALL.md#docker). [#76](https://github.com/terrastruct/d2/issues/76)
|
||||
- Adds `.msi` installer for convenient installation on Windows. [#379](https://github.com/terrastruct/d2/issues/379)
|
||||
|
||||
#### Improvements 🧹
|
||||
|
||||
- `d2 fmt` preserves leading comment spacing. [#400](https://github.com/terrastruct/d2/issues/400)
|
||||
- `stroke` and `fill` keywords work for Markdown text. [https://github.com/terrastruct/d2/pull/460](https://github.com/terrastruct/d2/pull/460)
|
||||
- PNG export resolution increased by 2x to not be blurry exporting on retina displays. [https://github.com/terrastruct/d2/pull/445](https://github.com/terrastruct/d2/pull/445)
|
||||
|
||||
#### Bugfixes ⛑️
|
||||
|
||||
- Fixes crash when sequence diagrams has no messages. [https://github.com/terrastruct/d2/pull/427](https://github.com/terrastruct/d2/pull/427)
|
||||
- Fixes `constraint` keyword setting label. [https://github.com/terrastruct/d2/issues/415](https://github.com/terrastruct/d2/issues/415)
|
||||
- Fixes serialization affecting binary plugins (TALA). [https://github.com/terrastruct/d2/pull/426](https://github.com/terrastruct/d2/pull/426)
|
||||
- Fixes connections in ELK layouts not going all the way to shape borders. [https://github.com/terrastruct/d2/pull/459](https://github.com/terrastruct/d2/pull/459)
|
||||
- Fixes a connection rendering bug that could happen in Firefox when there were no connection labels. [https://github.com/terrastruct/d2/pull/453](https://github.com/terrastruct/d2/pull/453)
|
||||
- Fixes a crash when external connection IDs were prefixes of a sequence diagram ID. [https://github.com/terrastruct/d2/pull/462](https://github.com/terrastruct/d2/pull/462)
|
||||
34
ci/release/changelogs/v0.1.3.md
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
Many have asked how to get the diagram to look like the one on D2's [cheat sheet](https://d2lang.com/tour/cheat-sheet). With this release, now you can! See [https://d2lang.com/tour/themes](https://d2lang.com/tour/themes) for more.
|
||||
|
||||

|
||||
|
||||
The Slack app for D2 has now hit production, so if you're looking for the quickest way to express a visual model without interrupting the conversation flow, go to [http://d2lang.com/tour/slack](http://d2lang.com/tour/slack) to install.
|
||||
|
||||
Hope everyone is enjoying the holidays this week!
|
||||
|
||||
#### Features 🚀
|
||||
|
||||
- `sketch` flag renders the diagram to look like it was sketched by hand. [#492](https://github.com/terrastruct/d2/pull/492)
|
||||
- `near` now takes constants like `top-center`, particularly useful for diagram titles. See [docs](https://d2lang.com/tour/text#near-a-constant) for more. [#525](https://github.com/terrastruct/d2/pull/525)
|
||||
|
||||
#### Improvements 🧹
|
||||
|
||||
- Improved label placements for shapes with images and icons to avoid overlapping labels. [#474](https://github.com/terrastruct/d2/pull/474)
|
||||
- Themes are applied to `sql_table` and `class` shapes. [#521](https://github.com/terrastruct/d2/pull/521)
|
||||
- `class` shapes use monospaced font. [#521](https://github.com/terrastruct/d2/pull/521)
|
||||
- Sequence diagram edge group labels have more reasonable padding. [#512](https://github.com/terrastruct/d2/pull/512)
|
||||
- ELK layout engine preserves order of nodes. [#282](https://github.com/terrastruct/d2/issues/282)
|
||||
- Non-markdown text (`shape: text` without language block) works with `bold`, `italic`, `underline`, and `font-size`. [#528](https://github.com/terrastruct/d2/pull/528)
|
||||
- Markdown headings set font-family explicitly, so that external stylesheets with more specific targeting don't override it. [#525](https://github.com/terrastruct/d2/pull/525)
|
||||
|
||||
#### Bugfixes ⛑️
|
||||
|
||||
- `d2 fmt` only rewrites if it has changes, instead of always rewriting. [#470](https://github.com/terrastruct/d2/pull/470)
|
||||
- Text no longer overflows in `sql_table` shapes. [#458](https://github.com/terrastruct/d2/pull/458)
|
||||
- ELK connection labels are now given the appropriate dimensions. [#483](https://github.com/terrastruct/d2/pull/483)
|
||||
- Dagre connection lengths make room for longer labels. [#484](https://github.com/terrastruct/d2/pull/484)
|
||||
- Icons with query parameters are escaped to valid SVG XML. [#438](https://github.com/terrastruct/d2/issues/438)
|
||||
- Connections at the boundaries no longer get part of its stroke clipped. [#493](https://github.com/terrastruct/d2/pull/493)
|
||||
- Fixes edge case where `style` being defined in same scope as `sql_table` causes compiler to skip compiling `sql_table`. [#506](https://github.com/terrastruct/d2/issues/506)
|
||||
- Fixes panic passing a non-string value to `constraint`. [#248](https://github.com/terrastruct/d2/issues/248)
|
||||
- Fixes edge case where the key `null` was compiling wrongly. [#507](https://github.com/terrastruct/d2/issues/507)
|
||||
|
|
@ -8,5 +8,5 @@ RUN curl -fsSL "https://go.dev/dl/go$GOVERSION.tar.gz" >/tmp/go.tar.gz
|
|||
RUN tar -C /usr/local -xzf /tmp/go.tar.gz
|
||||
ENV PATH="/usr/local/go/bin:$PATH"
|
||||
|
||||
RUN apt-get install -y build-essential
|
||||
RUN apt-get install -y rsync
|
||||
RUN apt-get install -y build-essential \
|
||||
rsync
|
||||
|
|
@ -1,12 +1,35 @@
|
|||
#!/bin/sh
|
||||
set -eu
|
||||
cd -- "$(dirname "$0")/../../.."
|
||||
. ./ci/sub/lib.sh
|
||||
|
||||
ensure_os
|
||||
ensure_arch
|
||||
cat <<EOF
|
||||
# d2
|
||||
|
||||
For docs, more installation options and the source code see https://oss.terrastruct.com/d2
|
||||
|
||||
version: $VERSION
|
||||
os: $OS
|
||||
arch: $ARCH
|
||||
|
||||
Built with $(go version | grep -o 'go[^ ]\+').
|
||||
EOF
|
||||
|
||||
if [ "$OS" = windows ]; then
|
||||
cat <<EOF
|
||||
|
||||
This release is structured the same as our Unix releases for use with MSYS2.
|
||||
|
||||
You may find our \`.msi\` installer more convenient as it handles putting \`d2.exe\` into
|
||||
your \`\$PATH\` for you.
|
||||
|
||||
See https://github.com/terrastruct/d2/blob/master/docs/INSTALL.md#windows
|
||||
EOF
|
||||
fi
|
||||
|
||||
cat <<EOF
|
||||
|
||||
## Install
|
||||
|
||||
|
|
|
|||
|
|
@ -58,6 +58,12 @@ Port listening address when used with
|
|||
Set the diagram theme to the passed integer. For a list of available options, see
|
||||
.Lk https://oss.terrastruct.com/d2
|
||||
.Ns .
|
||||
.It Fl s , -sketch Ar false
|
||||
Renders the diagram to look like it was sketched by hand
|
||||
.Ns .
|
||||
.It Fl -pad Ar 100
|
||||
Pixels padded around the rendered diagram
|
||||
.Ns .
|
||||
.It Fl l , -layout Ar dagre
|
||||
Set the diagram layout engine to the passed string. For a list of available options, run
|
||||
.Ar layout
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ main() {
|
|||
|
||||
ensure_os
|
||||
if [ "$OS" = windows ]; then
|
||||
"$sh_c" install ./bin/d2 "$PREFIX/bin/d2.exe"
|
||||
"$sh_c" install ./bin/d2.exe "$PREFIX/bin/d2.exe"
|
||||
else
|
||||
"$sh_c" install ./bin/d2 "$PREFIX/bin/d2"
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -50,20 +50,16 @@ ensure_tmpdir() {
|
|||
if [ -n "${_TMPDIR-}" ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
_TMPDIR=$(mktemp -d)
|
||||
export _TMPDIR
|
||||
trap temp_exittrap EXIT
|
||||
}
|
||||
|
||||
temp_exittrap() {
|
||||
if [ -n "${_TMPDIR-}" ]; then
|
||||
rm -r "$_TMPDIR"
|
||||
fi
|
||||
}
|
||||
if [ -z "${_TMPDIR-}" ]; then
|
||||
trap 'rm -Rf "$_TMPDIR"' EXIT
|
||||
fi
|
||||
ensure_tmpdir
|
||||
|
||||
temppath() {
|
||||
ensure_tmpdir
|
||||
while true; do
|
||||
temppath=$_TMPDIR/$(</dev/urandom od -N8 -tx -An -v | tr -d '[:space:]')
|
||||
if [ ! -e "$temppath" ]; then
|
||||
|
|
@ -121,10 +117,14 @@ should_color() {
|
|||
}
|
||||
|
||||
setaf() {
|
||||
tput setaf "$1"
|
||||
fg=$1
|
||||
shift
|
||||
printf '%s' "$*"
|
||||
tput sgr0
|
||||
printf '%s\n' "$*" | while IFS= read -r line; do
|
||||
tput setaf "$fg"
|
||||
printf '%s' "$line"
|
||||
tput sgr0
|
||||
printf '\n'
|
||||
done
|
||||
}
|
||||
|
||||
_echo() {
|
||||
|
|
@ -273,7 +273,7 @@ header() {
|
|||
|
||||
bigheader() {
|
||||
set -- "$(echo "$*" | sed "s/^/ * /")"
|
||||
FGCOLOR=${FGCOLOR:-3} logp "/****************************************************************
|
||||
FGCOLOR=${FGCOLOR:-6} logp "/****************************************************************
|
||||
$*
|
||||
****************************************************************/"
|
||||
}
|
||||
|
|
@ -298,6 +298,16 @@ hide() {
|
|||
return "$code"
|
||||
}
|
||||
|
||||
hide_stderr() {
|
||||
out="$(mktempd)/hideout"
|
||||
capcode "$@" 2>"$out"
|
||||
if [ "$code" -eq 0 ]; then
|
||||
return
|
||||
fi
|
||||
cat "$out" >&2
|
||||
return "$code"
|
||||
}
|
||||
|
||||
echo_dur() {
|
||||
local dur=$1
|
||||
local h=$((dur/60/60))
|
||||
|
|
|
|||
1
ci/release/windows/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
d2.exe
|
||||
BIN
ci/release/windows/d2.ico
Normal file
|
After Width: | Height: | Size: 95 KiB |
BIN
ci/release/windows/d2.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
39
ci/release/windows/d2.wxs
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
<!--
|
||||
Wix documentation is nonexistent for v4. What exists is largely out of date and inconsistent.
|
||||
This file was pieced together from:
|
||||
1. https://www.firegiant.com/wix/tutorial/getting-started/
|
||||
- This is for v3, I used wix convert to convert to v4
|
||||
2. https://wixtoolset.org/docs/reference/schema/wxs/
|
||||
3. Googling with trial and error
|
||||
-->
|
||||
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
|
||||
<Package Name="D2" UpgradeCode="ac84fee7-eb67-4f5d-a08d-adef69538690" Language="1033" Codepage="utf-8" Version="$(var.D2Version)" Manufacturer="Terrastruct, Inc." InstallerVersion="200">
|
||||
<SummaryInformation Keywords="Installer" Description="The D2 Installer" Manufacturer="Terrastruct, Inc." Codepage="1252" />
|
||||
<Icon Id="d2.ico" SourceFile="d2.ico" />
|
||||
<Property Id="ARPPRODUCTICON" Value="d2.ico" />
|
||||
|
||||
<Media Id="1" Cabinet="D2.cab" EmbedCab="yes" />
|
||||
|
||||
<Feature Id="Complete" Level="1">
|
||||
<ComponentRef Id="Executable" />
|
||||
</Feature>
|
||||
|
||||
<MajorUpgrade AllowSameVersionUpgrades='yes' DowngradeErrorMessage="A later version of [ProductName] is already installed. Setup will now exit."/>
|
||||
|
||||
<StandardDirectory Id="ProgramFiles64Folder">
|
||||
<Directory Id="INSTALLDIR" Name="D2">
|
||||
<Component Id="Executable" Guid="1090d036-c985-461f-94f6-3121dbcfcb48">
|
||||
<File Id="D2EXE" Name="d2.exe" Source="d2.exe" KeyPath="yes" />
|
||||
<Environment
|
||||
Id="D2PathEntry"
|
||||
Action="set"
|
||||
Part="last"
|
||||
Name="PATH"
|
||||
Permanent="no"
|
||||
System="yes"
|
||||
Value="[INSTALLDIR]" />
|
||||
</Component>
|
||||
</Directory>
|
||||
</StandardDirectory>
|
||||
</Package>
|
||||
</Wix>
|
||||
|
|
@ -120,7 +120,7 @@ func test(t *testing.T, textPath, text string) {
|
|||
ruler, err := textmeasure.NewRuler()
|
||||
assert.Nil(t, err)
|
||||
|
||||
err = g.SetDimensions(nil, ruler)
|
||||
err = g.SetDimensions(nil, ruler, nil)
|
||||
assert.Nil(t, err)
|
||||
|
||||
err = d2dagrelayout.Layout(ctx, g)
|
||||
|
|
@ -128,7 +128,7 @@ func test(t *testing.T, textPath, text string) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = d2exporter.Export(ctx, g, 0)
|
||||
_, err = d2exporter.Export(ctx, g, 0, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -363,6 +363,9 @@ func (c *compiler) applyScalar(attrs *d2graph.Attributes, reserved string, box d
|
|||
}
|
||||
attrs.Direction.Value = scalar.ScalarString()
|
||||
return
|
||||
case "constraint":
|
||||
// Compilation for shape-specific keywords happens elsewhere
|
||||
return
|
||||
}
|
||||
|
||||
if _, ok := d2graph.StyleKeywords[reserved]; ok {
|
||||
|
|
@ -580,16 +583,25 @@ func (c *compiler) compileShapes(obj *d2graph.Object) {
|
|||
c.compileShapes(obj)
|
||||
}
|
||||
|
||||
for _, obj := range obj.ChildrenArray {
|
||||
switch obj.Attributes.Shape.Value {
|
||||
for i := 0; i < len(obj.ChildrenArray); i++ {
|
||||
ch := obj.ChildrenArray[i]
|
||||
switch ch.Attributes.Shape.Value {
|
||||
case d2target.ShapeClass, d2target.ShapeSQLTable:
|
||||
flattenContainer(obj.Graph, obj)
|
||||
flattenContainer(obj.Graph, ch)
|
||||
}
|
||||
if obj.IDVal == "style" {
|
||||
obj.Parent.Attributes.Style = obj.Attributes.Style
|
||||
if ch.IDVal == "style" {
|
||||
obj.Attributes.Style = ch.Attributes.Style
|
||||
if obj.Graph != nil {
|
||||
flattenContainer(obj.Graph, obj)
|
||||
removeObject(obj.Graph, obj)
|
||||
flattenContainer(obj.Graph, ch)
|
||||
for i := 0; i < len(obj.Graph.Objects); i++ {
|
||||
if obj.Graph.Objects[i] == ch {
|
||||
obj.Graph.Objects = append(obj.Graph.Objects[:i], obj.Graph.Objects[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
delete(obj.Children, ch.ID)
|
||||
obj.ChildrenArray = append(obj.ChildrenArray[:i], obj.ChildrenArray[i+1:]...)
|
||||
i--
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -666,8 +678,8 @@ func (c *compiler) compileSQLTable(obj *d2graph.Object) {
|
|||
typ = ""
|
||||
}
|
||||
d2Col := d2target.SQLColumn{
|
||||
Name: col.IDVal,
|
||||
Type: typ,
|
||||
Name: d2target.Text{Label: col.IDVal},
|
||||
Type: d2target.Text{Label: typ},
|
||||
}
|
||||
// The only map a sql table field could have is to specify constraint
|
||||
if col.Map != nil {
|
||||
|
|
@ -676,6 +688,10 @@ func (c *compiler) compileSQLTable(obj *d2graph.Object) {
|
|||
continue
|
||||
}
|
||||
if n.MapKey.Key.Path[0].Unbox().ScalarString() == "constraint" {
|
||||
if n.MapKey.Value.StringBox().Unbox() == nil {
|
||||
c.errorf(n.MapKey.GetRange().Start, n.MapKey.GetRange().End, "constraint value must be a string")
|
||||
return
|
||||
}
|
||||
d2Col.Constraint = n.MapKey.Value.StringBox().Unbox().ScalarString()
|
||||
}
|
||||
}
|
||||
|
|
@ -703,23 +719,6 @@ func (c *compiler) compileSQLTable(obj *d2graph.Object) {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO too similar to flattenContainer, should reconcile in a refactor
|
||||
func removeObject(g *d2graph.Graph, obj *d2graph.Object) {
|
||||
for i := 0; i < len(obj.Graph.Objects); i++ {
|
||||
if obj.Graph.Objects[i] == obj {
|
||||
obj.Graph.Objects = append(obj.Graph.Objects[:i], obj.Graph.Objects[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
delete(obj.Parent.Children, obj.ID)
|
||||
for i, child := range obj.Parent.ChildrenArray {
|
||||
if obj == child {
|
||||
obj.Parent.ChildrenArray = append(obj.Parent.ChildrenArray[:i], obj.Parent.ChildrenArray[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func flattenContainer(g *d2graph.Graph, obj *d2graph.Object) {
|
||||
absID := obj.AbsID()
|
||||
|
||||
|
|
@ -798,15 +797,18 @@ func (c *compiler) validateKey(obj *d2graph.Object, m *d2ast.Map, mk *d2ast.Key)
|
|||
return
|
||||
}
|
||||
|
||||
if reserved == "" && obj.Attributes.Shape.Value == d2target.ShapeImage {
|
||||
c.errorf(mk.Range.Start, mk.Range.End, "image shapes cannot have children.")
|
||||
}
|
||||
switch strings.ToLower(obj.Attributes.Shape.Value) {
|
||||
case d2target.ShapeImage:
|
||||
if reserved == "" {
|
||||
c.errorf(mk.Range.Start, mk.Range.End, "image shapes cannot have children.")
|
||||
}
|
||||
case d2target.ShapeCircle, d2target.ShapeSquare:
|
||||
checkEqual := (reserved == "width" && obj.Attributes.Height != nil) ||
|
||||
(reserved == "height" && obj.Attributes.Width != nil)
|
||||
|
||||
if reserved == "width" && obj.Attributes.Shape.Value != d2target.ShapeImage {
|
||||
c.errorf(mk.Range.Start, mk.Range.End, "width is only applicable to image shapes.")
|
||||
}
|
||||
if reserved == "height" && obj.Attributes.Shape.Value != d2target.ShapeImage {
|
||||
c.errorf(mk.Range.Start, mk.Range.End, "height is only applicable to image shapes.")
|
||||
if checkEqual && obj.Attributes.Width.Value != obj.Attributes.Height.Value {
|
||||
c.errorf(mk.Range.Start, mk.Range.End, fmt.Sprintf("width and height must be equal for %s shapes", obj.Attributes.Shape.Value))
|
||||
}
|
||||
}
|
||||
|
||||
in := d2target.IsShape(obj.Attributes.Shape.Value)
|
||||
|
|
@ -831,6 +833,14 @@ func (c *compiler) validateKey(obj *d2graph.Object, m *d2ast.Map, mk *d2ast.Key)
|
|||
return
|
||||
}
|
||||
|
||||
switch strings.ToLower(obj.Attributes.Shape.Value) {
|
||||
case d2target.ShapeSQLTable, d2target.ShapeClass:
|
||||
default:
|
||||
if len(obj.Children) > 0 && (reserved == "width" || reserved == "height") {
|
||||
c.errorf(mk.Range.Start, mk.Range.End, fmt.Sprintf("%s cannot be used on container: %s", reserved, obj.AbsID()))
|
||||
}
|
||||
}
|
||||
|
||||
if len(mk.Edges) > 0 {
|
||||
return
|
||||
}
|
||||
|
|
@ -851,11 +861,33 @@ func (c *compiler) validateKeys(obj *d2graph.Object, m *d2ast.Map) {
|
|||
func (c *compiler) validateNear(g *d2graph.Graph) {
|
||||
for _, obj := range g.Objects {
|
||||
if obj.Attributes.NearKey != nil {
|
||||
_, ok := g.Root.HasChild(d2graph.Key(obj.Attributes.NearKey))
|
||||
if !ok {
|
||||
c.errorf(obj.Attributes.NearKey.GetRange().Start, obj.Attributes.NearKey.GetRange().End, "near key %#v does not exist. It must be the absolute path to a shape.", d2format.Format(obj.Attributes.NearKey))
|
||||
_, 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.GetRange().Start, obj.Attributes.NearKey.GetRange().End, "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.GetRange().Start, obj.Attributes.NearKey.GetRange().End, "constant near keys can only be set on root level shapes")
|
||||
continue
|
||||
}
|
||||
if !isKey && isConst && len(obj.ChildrenArray) > 0 {
|
||||
c.errorf(obj.Attributes.NearKey.GetRange().Start, obj.Attributes.NearKey.GetRange().End, "constant near keys cannot be set on shapes with children")
|
||||
continue
|
||||
}
|
||||
if !isKey && isConst {
|
||||
is := false
|
||||
for _, e := range g.Edges {
|
||||
if e.Src == obj || e.Dst == obj {
|
||||
is = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if is {
|
||||
c.errorf(obj.Attributes.NearKey.GetRange().Start, obj.Attributes.NearKey.GetRange().End, "constant near keys cannot be set on connected shapes")
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,13 +7,13 @@ import (
|
|||
"testing"
|
||||
|
||||
tassert "github.com/stretchr/testify/assert"
|
||||
"oss.terrastruct.com/util-go/assert"
|
||||
"oss.terrastruct.com/util-go/diff"
|
||||
|
||||
"oss.terrastruct.com/d2/d2compiler"
|
||||
"oss.terrastruct.com/d2/d2format"
|
||||
"oss.terrastruct.com/d2/d2graph"
|
||||
"oss.terrastruct.com/d2/d2target"
|
||||
"oss.terrastruct.com/util-go/assert"
|
||||
"oss.terrastruct.com/util-go/diff"
|
||||
)
|
||||
|
||||
func TestCompile(t *testing.T) {
|
||||
|
|
@ -95,8 +95,119 @@ x: {
|
|||
height: 230
|
||||
}
|
||||
`,
|
||||
expErr: `d2/testdata/d2compiler/TestCompile/dimensions_on_nonimage.d2:3:2: width is only applicable to image shapes.
|
||||
d2/testdata/d2compiler/TestCompile/dimensions_on_nonimage.d2:4:2: height is only applicable to image shapes.
|
||||
assertions: func(t *testing.T, g *d2graph.Graph) {
|
||||
if len(g.Objects) != 1 {
|
||||
t.Fatalf("expected 1 objects: %#v", g.Objects)
|
||||
}
|
||||
if g.Objects[0].ID != "hey" {
|
||||
t.Fatalf("expected g.Objects[0].ID to be 'hey': %#v", g.Objects[0])
|
||||
}
|
||||
if g.Objects[0].Attributes.Shape.Value != d2target.ShapeHexagon {
|
||||
t.Fatalf("expected g.Objects[0].Attributes.Shape.Value to be hexagon: %#v", g.Objects[0].Attributes.Shape.Value)
|
||||
}
|
||||
if g.Objects[0].Attributes.Width.Value != "200" {
|
||||
t.Fatalf("expected g.Objects[0].Attributes.Width.Value to be 200: %#v", g.Objects[0].Attributes.Width.Value)
|
||||
}
|
||||
if g.Objects[0].Attributes.Height.Value != "230" {
|
||||
t.Fatalf("expected g.Objects[0].Attributes.Height.Value to be 230: %#v", g.Objects[0].Attributes.Height.Value)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "equal_dimensions_on_circle",
|
||||
|
||||
text: `hey: "" {
|
||||
shape: circle
|
||||
width: 200
|
||||
height: 230
|
||||
}
|
||||
`,
|
||||
expErr: `d2/testdata/d2compiler/TestCompile/equal_dimensions_on_circle.d2:3:2: width and height must be equal for circle shapes
|
||||
d2/testdata/d2compiler/TestCompile/equal_dimensions_on_circle.d2:4:2: width and height must be equal for circle shapes
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "single_dimension_on_circle",
|
||||
|
||||
text: `hey: "" {
|
||||
shape: circle
|
||||
height: 230
|
||||
}
|
||||
`,
|
||||
assertions: func(t *testing.T, g *d2graph.Graph) {
|
||||
if len(g.Objects) != 1 {
|
||||
t.Fatalf("expected 1 objects: %#v", g.Objects)
|
||||
}
|
||||
if g.Objects[0].ID != "hey" {
|
||||
t.Fatalf("expected ID to be 'hey': %#v", g.Objects[0])
|
||||
}
|
||||
if g.Objects[0].Attributes.Shape.Value != d2target.ShapeCircle {
|
||||
t.Fatalf("expected Attributes.Shape.Value to be circle: %#v", g.Objects[0].Attributes.Shape.Value)
|
||||
}
|
||||
if g.Objects[0].Attributes.Width != nil {
|
||||
t.Fatalf("expected Attributes.Width to be nil: %#v", g.Objects[0].Attributes.Width)
|
||||
}
|
||||
if g.Objects[0].Attributes.Height == nil {
|
||||
t.Fatalf("Attributes.Height is nil")
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no_dimensions_on_containers",
|
||||
|
||||
text: `
|
||||
containers: {
|
||||
circle container: {
|
||||
shape: circle
|
||||
width: 512
|
||||
|
||||
diamond: {
|
||||
shape: diamond
|
||||
width: 128
|
||||
height: 64
|
||||
}
|
||||
}
|
||||
diamond container: {
|
||||
shape: diamond
|
||||
width: 512
|
||||
height: 256
|
||||
|
||||
circle: {
|
||||
shape: circle
|
||||
width: 128
|
||||
}
|
||||
}
|
||||
oval container: {
|
||||
shape: oval
|
||||
width: 512
|
||||
height: 256
|
||||
|
||||
hexagon: {
|
||||
shape: hexagon
|
||||
width: 128
|
||||
height: 64
|
||||
}
|
||||
}
|
||||
hexagon container: {
|
||||
shape: hexagon
|
||||
width: 512
|
||||
height: 256
|
||||
|
||||
oval: {
|
||||
shape: oval
|
||||
width: 128
|
||||
height: 64
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
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
|
||||
`,
|
||||
},
|
||||
{
|
||||
|
|
@ -1266,6 +1377,50 @@ x -> y: {
|
|||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "near_constant",
|
||||
|
||||
text: `x.near: top-center
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "near_bad_constant",
|
||||
|
||||
text: `x.near: txop-center
|
||||
`,
|
||||
expErr: `d2/testdata/d2compiler/TestCompile/near_bad_constant.d2:1:1: near key "txop-center" must be the absolute path to a shape or one of the following constants: top-left, top-center, top-right, center-left, center-right, bottom-left, bottom-center, bottom-right
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "near_bad_container",
|
||||
|
||||
text: `x: {
|
||||
near: top-center
|
||||
y
|
||||
}
|
||||
`,
|
||||
expErr: `d2/testdata/d2compiler/TestCompile/near_bad_container.d2:1:1: constant near keys cannot be set on shapes with children
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "near_bad_connected",
|
||||
|
||||
text: `x: {
|
||||
near: top-center
|
||||
}
|
||||
x -> y
|
||||
`,
|
||||
expErr: `d2/testdata/d2compiler/TestCompile/near_bad_connected.d2:1:1: constant near keys cannot be set on connected shapes
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "nested_near_constant",
|
||||
|
||||
text: `x.y.near: top-center
|
||||
`,
|
||||
expErr: `d2/testdata/d2compiler/TestCompile/nested_near_constant.d2:1:1: constant near keys can only be set on root level shapes
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "reserved_icon_near_style",
|
||||
|
||||
|
|
@ -1312,7 +1467,7 @@ y
|
|||
expErr: `d2/testdata/d2compiler/TestCompile/errors/reserved_icon_style.d2:3:9: bad icon url "::????:::%%orange": parse "::????:::%%orange": missing protocol scheme
|
||||
d2/testdata/d2compiler/TestCompile/errors/reserved_icon_style.d2:4:18: expected "opacity" to be a number between 0.0 and 1.0
|
||||
d2/testdata/d2compiler/TestCompile/errors/reserved_icon_style.d2:5:18: expected "opacity" to be a number between 0.0 and 1.0
|
||||
d2/testdata/d2compiler/TestCompile/errors/reserved_icon_style.d2:1:1: near key "y" does not exist. It must be the absolute path to a shape.
|
||||
d2/testdata/d2compiler/TestCompile/errors/reserved_icon_style.d2:1:1: near key "y" must be the absolute path to a shape or one of the following constants: top-left, top-center, top-right, center-left, center-right, bottom-left, bottom-center, bottom-right
|
||||
`,
|
||||
},
|
||||
{
|
||||
|
|
@ -1465,8 +1620,8 @@ b`, g.Objects[0].Attributes.Label.Value)
|
|||
if len(g.Objects) != 1 {
|
||||
t.Fatal(g.Objects)
|
||||
}
|
||||
assert.String(t, `GetType()`, g.Objects[0].SQLTable.Columns[0].Name)
|
||||
assert.String(t, `Is()`, g.Objects[0].SQLTable.Columns[1].Name)
|
||||
assert.String(t, `GetType()`, g.Objects[0].SQLTable.Columns[0].Name.Label)
|
||||
assert.String(t, `Is()`, g.Objects[0].SQLTable.Columns[1].Name.Label)
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -1490,8 +1645,8 @@ b`, g.Objects[0].Attributes.Label.Value)
|
|||
if len(g.Objects[0].ChildrenArray) != 1 {
|
||||
t.Fatal(g.Objects)
|
||||
}
|
||||
assert.String(t, `GetType()`, g.Objects[1].SQLTable.Columns[0].Name)
|
||||
assert.String(t, `Is()`, g.Objects[1].SQLTable.Columns[1].Name)
|
||||
assert.String(t, `GetType()`, g.Objects[1].SQLTable.Columns[0].Name.Label)
|
||||
assert.String(t, `Is()`, g.Objects[1].SQLTable.Columns[1].Name.Label)
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -1618,6 +1773,17 @@ choo: {
|
|||
assert.String(t, "left", g.Objects[0].Attributes.Direction.Value)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "constraint_label",
|
||||
|
||||
text: `foo {
|
||||
label: bar
|
||||
constraint: BIZ
|
||||
}`,
|
||||
assertions: func(t *testing.T, g *d2graph.Graph) {
|
||||
assert.String(t, "bar", g.Objects[0].Attributes.Label.Value)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid_direction",
|
||||
|
||||
|
|
@ -1639,6 +1805,44 @@ choo: {
|
|||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "null",
|
||||
|
||||
text: `null
|
||||
`,
|
||||
assertions: func(t *testing.T, g *d2graph.Graph) {
|
||||
tassert.Equal(t, "'null'", g.Objects[0].ID)
|
||||
tassert.Equal(t, "null", g.Objects[0].IDVal)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "sql-regression",
|
||||
|
||||
text: `a: {
|
||||
style: {
|
||||
fill: lemonchiffon
|
||||
}
|
||||
b: {
|
||||
shape: sql_table
|
||||
c
|
||||
}
|
||||
d
|
||||
}
|
||||
`,
|
||||
assertions: func(t *testing.T, g *d2graph.Graph) {
|
||||
tassert.Equal(t, 3, len(g.Objects))
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "sql-panic",
|
||||
text: `test {
|
||||
shape: sql_table
|
||||
test_id: varchar(64) {constraint: [primary_key, foreign_key]}
|
||||
}
|
||||
`,
|
||||
expErr: `d2/testdata/d2compiler/TestCompile/sql-panic.d2:3:27: constraint value must be a string
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
|
|
|
|||
|
|
@ -5,15 +5,21 @@ import (
|
|||
"strconv"
|
||||
|
||||
"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/util-go/go2"
|
||||
)
|
||||
|
||||
func Export(ctx context.Context, g *d2graph.Graph, themeID int64) (*d2target.Diagram, error) {
|
||||
func Export(ctx context.Context, g *d2graph.Graph, themeID int64, fontFamily *d2fonts.FontFamily) (*d2target.Diagram, error) {
|
||||
theme := d2themescatalog.Find(themeID)
|
||||
|
||||
diagram := d2target.NewDiagram()
|
||||
if fontFamily == nil {
|
||||
fontFamily = go2.Pointer(d2fonts.SourceSansPro)
|
||||
}
|
||||
diagram.FontFamily = fontFamily
|
||||
|
||||
diagram.Shapes = make([]d2target.Shape, len(g.Objects))
|
||||
for i := range g.Objects {
|
||||
|
|
@ -34,6 +40,11 @@ func applyTheme(shape *d2target.Shape, obj *d2graph.Object, theme *d2themes.Them
|
|||
if obj.Attributes.Shape.Value == d2target.ShapeText {
|
||||
shape.Color = theme.Colors.Neutrals.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
|
||||
}
|
||||
}
|
||||
|
||||
func applyStyles(shape *d2target.Shape, obj *d2graph.Object) {
|
||||
|
|
@ -45,6 +56,8 @@ func applyStyles(shape *d2target.Shape, obj *d2graph.Object) {
|
|||
}
|
||||
if obj.Attributes.Style.Fill != nil {
|
||||
shape.Fill = obj.Attributes.Style.Fill.Value
|
||||
} else if obj.Attributes.Shape.Value == d2target.ShapeText {
|
||||
shape.Fill = "transparent"
|
||||
}
|
||||
if obj.Attributes.Style.Stroke != nil {
|
||||
shape.Stroke = obj.Attributes.Style.Stroke.Value
|
||||
|
|
@ -68,19 +81,17 @@ func applyStyles(shape *d2target.Shape, obj *d2graph.Object) {
|
|||
if obj.Attributes.Style.FontColor != nil {
|
||||
shape.Color = obj.Attributes.Style.FontColor.Value
|
||||
}
|
||||
if obj.Attributes.Shape.Value != d2target.ShapeText {
|
||||
if obj.Attributes.Style.Italic != nil {
|
||||
shape.Italic, _ = strconv.ParseBool(obj.Attributes.Style.Italic.Value)
|
||||
}
|
||||
if obj.Attributes.Style.Bold != nil {
|
||||
shape.Bold, _ = strconv.ParseBool(obj.Attributes.Style.Bold.Value)
|
||||
}
|
||||
if obj.Attributes.Style.Underline != nil {
|
||||
shape.Underline, _ = strconv.ParseBool(obj.Attributes.Style.Underline.Value)
|
||||
}
|
||||
if obj.Attributes.Style.Font != nil {
|
||||
shape.FontFamily = obj.Attributes.Style.Font.Value
|
||||
}
|
||||
if obj.Attributes.Style.Italic != nil {
|
||||
shape.Italic, _ = strconv.ParseBool(obj.Attributes.Style.Italic.Value)
|
||||
}
|
||||
if obj.Attributes.Style.Bold != nil {
|
||||
shape.Bold, _ = strconv.ParseBool(obj.Attributes.Style.Bold.Value)
|
||||
}
|
||||
if obj.Attributes.Style.Underline != nil {
|
||||
shape.Underline, _ = strconv.ParseBool(obj.Attributes.Style.Underline.Value)
|
||||
}
|
||||
if obj.Attributes.Style.Font != nil {
|
||||
shape.FontFamily = obj.Attributes.Style.Font.Value
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -200,6 +211,10 @@ func toConnection(edge *d2graph.Edge, theme *d2themes.Theme) d2target.Connection
|
|||
connection.StrokeWidth, _ = strconv.Atoi(edge.Attributes.Style.StrokeWidth.Value)
|
||||
}
|
||||
|
||||
if edge.Attributes.Style.Fill != nil {
|
||||
connection.Fill = edge.Attributes.Style.Fill.Value
|
||||
}
|
||||
|
||||
connection.FontSize = text.FontSize
|
||||
if edge.Attributes.Style.FontSize != nil {
|
||||
connection.FontSize, _ = strconv.Atoi(edge.Attributes.Style.FontSize.Value)
|
||||
|
|
|
|||
|
|
@ -8,12 +8,15 @@ import (
|
|||
|
||||
"cdr.dev/slog"
|
||||
|
||||
tassert "github.com/stretchr/testify/assert"
|
||||
|
||||
"oss.terrastruct.com/util-go/assert"
|
||||
"oss.terrastruct.com/util-go/diff"
|
||||
|
||||
"oss.terrastruct.com/d2/d2compiler"
|
||||
"oss.terrastruct.com/d2/d2exporter"
|
||||
"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"
|
||||
|
|
@ -80,6 +83,23 @@ y: {shape: square}
|
|||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "sequence_group_position",
|
||||
|
||||
dsl: `hey {
|
||||
shape: sequence_diagram
|
||||
a
|
||||
b
|
||||
group: {
|
||||
a -> b
|
||||
}
|
||||
}
|
||||
`,
|
||||
assertions: func(t *testing.T, d *d2target.Diagram) {
|
||||
tassert.Equal(t, "hey.group", d.Shapes[3].ID)
|
||||
tassert.Equal(t, "INSIDE_TOP_LEFT", d.Shapes[3].LabelPosition)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
runa(t, tcs)
|
||||
|
|
@ -216,15 +236,15 @@ func run(t *testing.T, tc testCase) {
|
|||
ruler, err := textmeasure.NewRuler()
|
||||
assert.JSON(t, nil, err)
|
||||
|
||||
err = g.SetDimensions(nil, ruler)
|
||||
err = g.SetDimensions(nil, ruler, nil)
|
||||
assert.JSON(t, nil, err)
|
||||
|
||||
err = d2dagrelayout.Layout(ctx, g)
|
||||
err = d2sequence.Layout(ctx, g, d2dagrelayout.Layout)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
got, err := d2exporter.Export(ctx, g, tc.themeID)
|
||||
got, err := d2exporter.Export(ctx, g, tc.themeID, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ func escapeUnquotedValue(s string, inKey bool) string {
|
|||
}
|
||||
|
||||
if strings.EqualFold(s, "null") {
|
||||
return "\\null"
|
||||
return `'null'`
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
|
|
|
|||
|
|
@ -170,7 +170,7 @@ func TestEscapeUnquoted(t *testing.T) {
|
|||
{
|
||||
name: "null",
|
||||
str: `null`,
|
||||
exp: `\null`,
|
||||
exp: `'null'`,
|
||||
},
|
||||
{
|
||||
name: "empty",
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ func (p *printer) comment(c *d2ast.Comment) {
|
|||
lines := strings.Split(c.Value, "\n")
|
||||
for i, line := range lines {
|
||||
p.sb.WriteString("#")
|
||||
if line != "" && !strings.HasPrefix(line, " ") {
|
||||
if line != "" {
|
||||
p.sb.WriteByte(' ')
|
||||
}
|
||||
p.sb.WriteString(line)
|
||||
|
|
|
|||
|
|
@ -592,6 +592,23 @@ hi # Fraud is the homage that force pays to reason.
|
|||
in: `x: {}
|
||||
`,
|
||||
exp: `x
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "leading_space_comments",
|
||||
in: `# foo
|
||||
# foobar
|
||||
# baz
|
||||
x -> y
|
||||
#foo
|
||||
y
|
||||
`,
|
||||
exp: `# foo
|
||||
# foobar
|
||||
# baz
|
||||
x -> y
|
||||
# foo
|
||||
y
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package d2graph
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
|
@ -21,6 +22,7 @@ import (
|
|||
)
|
||||
|
||||
const INNER_LABEL_PADDING int = 5
|
||||
const DEFAULT_SHAPE_PADDING = 100.
|
||||
|
||||
// TODO: Refactor with a light abstract layer on top of AST implementing scenarios,
|
||||
// variables, imports, substitutions and then a final set of structures representing
|
||||
|
|
@ -392,14 +394,23 @@ func (obj *Object) GetFill(theme *d2themes.Theme) string {
|
|||
return theme.Colors.Neutrals.N5
|
||||
}
|
||||
|
||||
if strings.EqualFold(shape, d2target.ShapeSQLTable) || strings.EqualFold(shape, d2target.ShapeClass) {
|
||||
return theme.Colors.Neutrals.N1
|
||||
}
|
||||
|
||||
return theme.Colors.Neutrals.N7
|
||||
}
|
||||
|
||||
func (obj *Object) GetStroke(theme *d2themes.Theme, dashGapSize interface{}) string {
|
||||
shape := obj.Attributes.Shape.Value
|
||||
if strings.EqualFold(shape, d2target.ShapeCode) || strings.EqualFold(shape, d2target.ShapeClass) || strings.EqualFold(shape, d2target.ShapeSQLTable) {
|
||||
if strings.EqualFold(shape, d2target.ShapeCode) ||
|
||||
strings.EqualFold(shape, d2target.ShapeText) {
|
||||
return theme.Colors.Neutrals.N1
|
||||
}
|
||||
if strings.EqualFold(shape, d2target.ShapeClass) ||
|
||||
strings.EqualFold(shape, d2target.ShapeSQLTable) {
|
||||
return theme.Colors.Neutrals.N7
|
||||
}
|
||||
if dashGapSize != 0.0 {
|
||||
return theme.Colors.B2
|
||||
}
|
||||
|
|
@ -432,7 +443,14 @@ func (obj *Object) AbsIDArray() []string {
|
|||
}
|
||||
|
||||
func (obj *Object) Text() *d2target.MText {
|
||||
isBold := !obj.IsContainer()
|
||||
isBold := !obj.IsContainer() && obj.Attributes.Shape.Value != "text"
|
||||
isItalic := false
|
||||
if obj.Attributes.Style.Bold != nil && obj.Attributes.Style.Bold.Value == "true" {
|
||||
isBold = true
|
||||
}
|
||||
if obj.Attributes.Style.Italic != nil && obj.Attributes.Style.Italic.Value == "true" {
|
||||
isItalic = true
|
||||
}
|
||||
fontSize := d2fonts.FONT_SIZE_M
|
||||
if obj.OuterSequenceDiagram() == nil {
|
||||
if obj.IsContainer() {
|
||||
|
|
@ -448,11 +466,14 @@ func (obj *Object) Text() *d2target.MText {
|
|||
if obj.Class != nil || obj.SQLTable != nil {
|
||||
fontSize = d2fonts.FONT_SIZE_XL
|
||||
}
|
||||
if obj.Class != nil {
|
||||
isBold = false
|
||||
}
|
||||
return &d2target.MText{
|
||||
Text: obj.Attributes.Label.Value,
|
||||
FontSize: fontSize,
|
||||
IsBold: isBold,
|
||||
IsItalic: false,
|
||||
IsItalic: isItalic,
|
||||
Language: obj.Attributes.Language,
|
||||
Shape: obj.Attributes.Shape.Value,
|
||||
|
||||
|
|
@ -649,6 +670,164 @@ func (obj *Object) AppendReferences(ida []string, ref Reference, unresolvedObj *
|
|||
}
|
||||
}
|
||||
|
||||
func (obj *Object) GetLabelSize(mtexts []*d2target.MText, ruler *textmeasure.Ruler, fontFamily *d2fonts.FontFamily) (*d2target.TextDimensions, error) {
|
||||
shapeType := strings.ToLower(obj.Attributes.Shape.Value)
|
||||
|
||||
var dims *d2target.TextDimensions
|
||||
switch shapeType {
|
||||
case d2target.ShapeText:
|
||||
if obj.Attributes.Language == "latex" {
|
||||
width, height, err := d2latex.Measure(obj.Text().Text)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dims = d2target.NewTextDimensions(width, height)
|
||||
} else if obj.Attributes.Language != "" {
|
||||
var err error
|
||||
dims, err = getMarkdownDimensions(mtexts, ruler, obj.Text(), fontFamily)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
dims = GetTextDimensions(mtexts, ruler, obj.Text(), fontFamily)
|
||||
}
|
||||
|
||||
case d2target.ShapeClass:
|
||||
dims = GetTextDimensions(mtexts, ruler, obj.Text(), go2.Pointer(d2fonts.SourceCodePro))
|
||||
|
||||
default:
|
||||
dims = GetTextDimensions(mtexts, ruler, obj.Text(), fontFamily)
|
||||
}
|
||||
|
||||
if shapeType == d2target.ShapeSQLTable && obj.Attributes.Label.Value == "" {
|
||||
// measure with placeholder text to determine height
|
||||
placeholder := *obj.Text()
|
||||
placeholder.Text = "Table"
|
||||
dims = GetTextDimensions(mtexts, ruler, &placeholder, fontFamily)
|
||||
}
|
||||
|
||||
if dims == nil {
|
||||
if shapeType == d2target.ShapeImage {
|
||||
dims = d2target.NewTextDimensions(0, 0)
|
||||
} else {
|
||||
return nil, fmt.Errorf("dimensions for object label %#v not found", obj.Text())
|
||||
}
|
||||
}
|
||||
|
||||
return dims, nil
|
||||
}
|
||||
|
||||
func (obj *Object) GetDefaultSize(mtexts []*d2target.MText, ruler *textmeasure.Ruler, fontFamily *d2fonts.FontFamily, labelDims d2target.TextDimensions) (*d2target.TextDimensions, error) {
|
||||
dims := d2target.TextDimensions{}
|
||||
|
||||
switch strings.ToLower(obj.Attributes.Shape.Value) {
|
||||
default:
|
||||
return d2target.NewTextDimensions(labelDims.Width, labelDims.Height), nil
|
||||
|
||||
case d2target.ShapeImage:
|
||||
return d2target.NewTextDimensions(128, 128), nil
|
||||
|
||||
case d2target.ShapeClass:
|
||||
maxWidth := labelDims.Width
|
||||
|
||||
for _, f := range obj.Class.Fields {
|
||||
fdims := GetTextDimensions(mtexts, ruler, f.Text(), go2.Pointer(d2fonts.SourceCodePro))
|
||||
if fdims == nil {
|
||||
return nil, fmt.Errorf("dimensions for class field %#v not found", f.Text())
|
||||
}
|
||||
lineWidth := fdims.Width
|
||||
if maxWidth < lineWidth {
|
||||
maxWidth = lineWidth
|
||||
}
|
||||
}
|
||||
for _, m := range obj.Class.Methods {
|
||||
mdims := GetTextDimensions(mtexts, ruler, m.Text(), go2.Pointer(d2fonts.SourceCodePro))
|
||||
if mdims == nil {
|
||||
return nil, fmt.Errorf("dimensions for class method %#v not found", m.Text())
|
||||
}
|
||||
lineWidth := mdims.Width
|
||||
if maxWidth < lineWidth {
|
||||
maxWidth = lineWidth
|
||||
}
|
||||
}
|
||||
dims.Width = maxWidth
|
||||
|
||||
// All rows should be the same height
|
||||
var anyRowText *d2target.MText
|
||||
if len(obj.Class.Fields) > 0 {
|
||||
anyRowText = obj.Class.Fields[0].Text()
|
||||
} else if len(obj.Class.Methods) > 0 {
|
||||
anyRowText = obj.Class.Methods[0].Text()
|
||||
}
|
||||
if anyRowText != nil {
|
||||
// 10px of padding top and bottom so text doesn't look squished
|
||||
rowHeight := GetTextDimensions(mtexts, ruler, anyRowText, go2.Pointer(d2fonts.SourceCodePro)).Height + 20
|
||||
dims.Height = rowHeight * (len(obj.Class.Fields) + len(obj.Class.Methods) + 2)
|
||||
} else {
|
||||
dims.Height = labelDims.Height
|
||||
}
|
||||
|
||||
case d2target.ShapeSQLTable:
|
||||
maxNameWidth := 0
|
||||
maxTypeWidth := 0
|
||||
constraintWidth := 0
|
||||
|
||||
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()
|
||||
|
||||
nameDims := GetTextDimensions(mtexts, ruler, ctexts[0], fontFamily)
|
||||
if nameDims == nil {
|
||||
return nil, fmt.Errorf("dimensions for sql_table name %#v not found", ctexts[0].Text)
|
||||
}
|
||||
c.Name.LabelWidth = nameDims.Width
|
||||
c.Name.LabelHeight = nameDims.Height
|
||||
if maxNameWidth < nameDims.Width {
|
||||
maxNameWidth = nameDims.Width
|
||||
}
|
||||
|
||||
typeDims := GetTextDimensions(mtexts, ruler, ctexts[1], fontFamily)
|
||||
if typeDims == nil {
|
||||
return nil, fmt.Errorf("dimensions for sql_table type %#v not found", ctexts[1].Text)
|
||||
}
|
||||
c.Type.LabelWidth = typeDims.Width
|
||||
c.Type.LabelHeight = typeDims.Height
|
||||
if maxTypeWidth < typeDims.Width {
|
||||
maxTypeWidth = typeDims.Width
|
||||
}
|
||||
|
||||
if c.Constraint != "" {
|
||||
// covers UNQ constraint with padding
|
||||
constraintWidth = 60
|
||||
}
|
||||
}
|
||||
|
||||
// The rows get padded a little due to header font being larger than row font
|
||||
dims.Height = labelDims.Height * (len(obj.SQLTable.Columns) + 1)
|
||||
headerWidth := d2target.HeaderPadding + labelDims.Width + d2target.HeaderPadding
|
||||
rowsWidth := d2target.NamePadding + maxNameWidth + d2target.TypePadding + maxTypeWidth + d2target.TypePadding + constraintWidth
|
||||
dims.Width = go2.Max(headerWidth, rowsWidth)
|
||||
}
|
||||
|
||||
return &dims, nil
|
||||
}
|
||||
|
||||
func (obj *Object) GetPadding() (x, y float64) {
|
||||
switch strings.ToLower(obj.Attributes.Shape.Value) {
|
||||
case d2target.ShapeImage,
|
||||
d2target.ShapeSQLTable,
|
||||
d2target.ShapeText,
|
||||
d2target.ShapeCode:
|
||||
return 0., 0.
|
||||
case d2target.ShapeClass:
|
||||
// TODO fix class row width measurements (see SQL table)
|
||||
return 100., 0.
|
||||
default:
|
||||
return DEFAULT_SHAPE_PADDING, DEFAULT_SHAPE_PADDING
|
||||
}
|
||||
}
|
||||
|
||||
type Edge struct {
|
||||
Index int `json:"index"`
|
||||
|
||||
|
|
@ -822,13 +1001,13 @@ func findMeasured(mtexts []*d2target.MText, t1 *d2target.MText) *d2target.TextDi
|
|||
return nil
|
||||
}
|
||||
|
||||
func getMarkdownDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler, t *d2target.MText) (*d2target.TextDimensions, error) {
|
||||
func getMarkdownDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler, t *d2target.MText, fontFamily *d2fonts.FontFamily) (*d2target.TextDimensions, error) {
|
||||
if dims := findMeasured(mtexts, t); dims != nil {
|
||||
return dims, nil
|
||||
}
|
||||
|
||||
if ruler != nil {
|
||||
width, height, err := textmeasure.MeasureMarkdown(t.Text, ruler)
|
||||
width, height, err := textmeasure.MeasureMarkdown(t.Text, ruler, fontFamily)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -838,7 +1017,7 @@ func getMarkdownDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler, t
|
|||
return nil, fmt.Errorf("text not pre-measured and no ruler provided")
|
||||
}
|
||||
|
||||
func getTextDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler, t *d2target.MText) *d2target.TextDimensions {
|
||||
func GetTextDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler, t *d2target.MText, fontFamily *d2fonts.FontFamily) *d2target.TextDimensions {
|
||||
if dims := findMeasured(mtexts, t); dims != nil {
|
||||
return dims
|
||||
}
|
||||
|
|
@ -858,7 +1037,10 @@ func getTextDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler, t *d2
|
|||
} else if t.IsItalic {
|
||||
style = d2fonts.FONT_STYLE_ITALIC
|
||||
}
|
||||
w, h = ruler.Measure(d2fonts.SourceSansPro.Font(t.FontSize, style), t.Text)
|
||||
if fontFamily == nil {
|
||||
fontFamily = go2.Pointer(d2fonts.SourceSansPro)
|
||||
}
|
||||
w, h = ruler.Measure(fontFamily.Font(t.FontSize, style), t.Text)
|
||||
}
|
||||
return d2target.NewTextDimensions(w, h)
|
||||
}
|
||||
|
|
@ -867,150 +1049,75 @@ func getTextDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler, t *d2
|
|||
}
|
||||
|
||||
func appendTextDedup(texts []*d2target.MText, t *d2target.MText) []*d2target.MText {
|
||||
if getTextDimensions(texts, nil, t) == nil {
|
||||
if GetTextDimensions(texts, nil, t, nil) == nil {
|
||||
return append(texts, t)
|
||||
}
|
||||
return texts
|
||||
}
|
||||
|
||||
func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler) error {
|
||||
func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler, fontFamily *d2fonts.FontFamily) error {
|
||||
for _, obj := range g.Objects {
|
||||
obj.Box = &geo.Box{}
|
||||
// TODO fix edge cases for unnamed class etc
|
||||
// Image shapes can set their own widths/heights
|
||||
if obj.Attributes.Label.Value == "" && obj.Attributes.Shape.Value != d2target.ShapeImage {
|
||||
obj.Width = 100
|
||||
obj.Height = 100
|
||||
continue
|
||||
|
||||
var desiredWidth int
|
||||
var desiredHeight int
|
||||
if obj.Attributes.Width != nil {
|
||||
desiredWidth, _ = strconv.Atoi(obj.Attributes.Width.Value)
|
||||
}
|
||||
if obj.Attributes.Height != nil {
|
||||
desiredHeight, _ = strconv.Atoi(obj.Attributes.Height.Value)
|
||||
}
|
||||
shapeType := strings.ToLower(obj.Attributes.Shape.Value)
|
||||
|
||||
labelDims, err := obj.GetLabelSize(mtexts, ruler, fontFamily)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var dims *d2target.TextDimensions
|
||||
var innerLabelPadding = INNER_LABEL_PADDING
|
||||
if obj.Attributes.Shape.Value == d2target.ShapeText {
|
||||
if obj.Attributes.Language == "latex" {
|
||||
width, height, err := d2latex.Measure(obj.Text().Text)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dims = d2target.NewTextDimensions(width, height)
|
||||
} else {
|
||||
var err error
|
||||
dims, err = getMarkdownDimensions(mtexts, ruler, obj.Text())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
innerLabelPadding = 0
|
||||
} else {
|
||||
dims = getTextDimensions(mtexts, ruler, obj.Text())
|
||||
}
|
||||
if dims == nil {
|
||||
if obj.Attributes.Shape.Value == d2target.ShapeImage {
|
||||
dims = d2target.NewTextDimensions(0, 0)
|
||||
} else {
|
||||
return fmt.Errorf("dimensions for object label %#v not found", obj.Text())
|
||||
}
|
||||
}
|
||||
|
||||
switch obj.Attributes.Shape.Value {
|
||||
switch shapeType {
|
||||
case d2target.ShapeText, d2target.ShapeClass, d2target.ShapeSQLTable, d2target.ShapeCode:
|
||||
// no labels
|
||||
default:
|
||||
if obj.Attributes.Label.Value != "" {
|
||||
obj.LabelWidth = go2.Pointer(dims.Width)
|
||||
obj.LabelHeight = go2.Pointer(dims.Height)
|
||||
obj.LabelWidth = go2.Pointer(labelDims.Width)
|
||||
obj.LabelHeight = go2.Pointer(labelDims.Height)
|
||||
}
|
||||
}
|
||||
|
||||
dims.Width += innerLabelPadding
|
||||
dims.Height += innerLabelPadding
|
||||
obj.LabelDimensions = *dims
|
||||
obj.Width = float64(dims.Width)
|
||||
obj.Height = float64(dims.Height)
|
||||
if shapeType != d2target.ShapeText && obj.Attributes.Label.Value != "" {
|
||||
labelDims.Width += INNER_LABEL_PADDING
|
||||
labelDims.Height += INNER_LABEL_PADDING
|
||||
}
|
||||
obj.LabelDimensions = *labelDims
|
||||
|
||||
switch strings.ToLower(obj.Attributes.Shape.Value) {
|
||||
default:
|
||||
obj.Width += 100
|
||||
obj.Height += 100
|
||||
defaultDims, err := obj.GetDefaultSize(mtexts, ruler, fontFamily, *labelDims)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case d2target.ShapeImage:
|
||||
if obj.Attributes.Width != nil {
|
||||
w, _ := strconv.Atoi(obj.Attributes.Width.Value)
|
||||
obj.Width = float64(w)
|
||||
} else {
|
||||
obj.Width = 128
|
||||
}
|
||||
if obj.Attributes.Height != nil {
|
||||
h, _ := strconv.Atoi(obj.Attributes.Height.Value)
|
||||
obj.Height = float64(h)
|
||||
} else {
|
||||
obj.Height = 128
|
||||
}
|
||||
obj.Width = float64(go2.Max(defaultDims.Width, desiredWidth))
|
||||
obj.Height = float64(go2.Max(defaultDims.Height, desiredHeight))
|
||||
|
||||
paddingX, paddingY := obj.GetPadding()
|
||||
|
||||
switch shapeType {
|
||||
case d2target.ShapeSquare, d2target.ShapeCircle:
|
||||
sideLength := go2.Max(obj.Width, obj.Height)
|
||||
obj.Width = sideLength + 100
|
||||
obj.Height = sideLength + 100
|
||||
|
||||
case d2target.ShapeClass:
|
||||
maxWidth := dims.Width
|
||||
|
||||
for _, f := range obj.Class.Fields {
|
||||
fdims := getTextDimensions(mtexts, ruler, f.Text())
|
||||
if fdims == nil {
|
||||
return fmt.Errorf("dimensions for class field %#v not found", f.Text())
|
||||
}
|
||||
lineWidth := fdims.Width
|
||||
if maxWidth < lineWidth {
|
||||
maxWidth = lineWidth
|
||||
}
|
||||
}
|
||||
for _, m := range obj.Class.Methods {
|
||||
mdims := getTextDimensions(mtexts, ruler, m.Text())
|
||||
if mdims == nil {
|
||||
return fmt.Errorf("dimensions for class method %#v not found", m.Text())
|
||||
}
|
||||
lineWidth := mdims.Width
|
||||
if maxWidth < lineWidth {
|
||||
maxWidth = lineWidth
|
||||
}
|
||||
if desiredWidth != 0 || desiredHeight != 0 {
|
||||
paddingX = 0.
|
||||
paddingY = 0.
|
||||
}
|
||||
|
||||
// All rows should be the same height
|
||||
var anyRowText *d2target.MText
|
||||
if len(obj.Class.Fields) > 0 {
|
||||
anyRowText = obj.Class.Fields[0].Text()
|
||||
} else if len(obj.Class.Methods) > 0 {
|
||||
anyRowText = obj.Class.Methods[0].Text()
|
||||
sideLength := math.Max(obj.Width+paddingX, obj.Height+paddingY)
|
||||
obj.Width = sideLength
|
||||
obj.Height = sideLength
|
||||
|
||||
default:
|
||||
if desiredWidth == 0 {
|
||||
obj.Width += float64(paddingX)
|
||||
}
|
||||
if anyRowText != nil {
|
||||
// 10px of padding top and bottom so text doesn't look squished
|
||||
rowHeight := getTextDimensions(mtexts, ruler, anyRowText).Height + 20
|
||||
obj.Height = float64(rowHeight * (len(obj.Class.Fields) + len(obj.Class.Methods) + 2))
|
||||
if desiredHeight == 0 {
|
||||
obj.Height += float64(paddingY)
|
||||
}
|
||||
// Leave room for padding
|
||||
obj.Width = float64(maxWidth + 100)
|
||||
|
||||
case d2target.ShapeSQLTable:
|
||||
maxWidth := dims.Width
|
||||
|
||||
for _, c := range obj.SQLTable.Columns {
|
||||
cdims := getTextDimensions(mtexts, ruler, c.Text())
|
||||
if cdims == nil {
|
||||
return fmt.Errorf("dimensions for column %#v not found", c.Text())
|
||||
}
|
||||
lineWidth := cdims.Width
|
||||
if maxWidth < lineWidth {
|
||||
maxWidth = lineWidth
|
||||
}
|
||||
}
|
||||
|
||||
// The rows get padded a little due to header font being larger than row font
|
||||
obj.Height = float64(dims.Height * (len(obj.SQLTable.Columns) + 1))
|
||||
// Leave room for padding
|
||||
obj.Width = float64(maxWidth + 100)
|
||||
|
||||
case d2target.ShapeText, d2target.ShapeCode:
|
||||
}
|
||||
}
|
||||
for _, edge := range g.Edges {
|
||||
|
|
@ -1025,7 +1132,7 @@ func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler
|
|||
for _, label := range endpointLabels {
|
||||
t := edge.Text()
|
||||
t.Text = label
|
||||
dims := getTextDimensions(mtexts, ruler, t)
|
||||
dims := GetTextDimensions(mtexts, ruler, t, fontFamily)
|
||||
edge.MinWidth += dims.Width
|
||||
// Some padding as it's not totally near the end
|
||||
edge.MinHeight += dims.Height + 5
|
||||
|
|
@ -1035,7 +1142,7 @@ func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler
|
|||
continue
|
||||
}
|
||||
|
||||
dims := getTextDimensions(mtexts, ruler, edge.Text())
|
||||
dims := GetTextDimensions(mtexts, ruler, edge.Text(), fontFamily)
|
||||
if dims == nil {
|
||||
return fmt.Errorf("dimensions for edge label %#v not found", edge.Text())
|
||||
}
|
||||
|
|
@ -1063,7 +1170,9 @@ func (g *Graph) Texts() []*d2target.MText {
|
|||
}
|
||||
} else if obj.SQLTable != nil {
|
||||
for _, column := range obj.SQLTable.Columns {
|
||||
texts = appendTextDedup(texts, column.Text())
|
||||
for _, t := range column.Texts() {
|
||||
texts = appendTextDedup(texts, t)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1149,6 +1258,22 @@ var StyleKeywords = map[string]struct{}{
|
|||
"filled": {},
|
||||
}
|
||||
|
||||
// TODO maybe autofmt should allow other values, and transform them to conform
|
||||
// e.g. left-center becomes center-left
|
||||
var NearConstantsArray = []string{
|
||||
"top-left",
|
||||
"top-center",
|
||||
"top-right",
|
||||
|
||||
"center-left",
|
||||
"center-right",
|
||||
|
||||
"bottom-left",
|
||||
"bottom-center",
|
||||
"bottom-right",
|
||||
}
|
||||
var NearConstants map[string]struct{}
|
||||
|
||||
func init() {
|
||||
for k, v := range StyleKeywords {
|
||||
ReservedKeywords[k] = v
|
||||
|
|
@ -1156,4 +1281,8 @@ func init() {
|
|||
for k, v := range ReservedKeywordHolders {
|
||||
ReservedKeywords[k] = v
|
||||
}
|
||||
NearConstants = make(map[string]struct{}, len(NearConstantsArray))
|
||||
for _, k := range NearConstantsArray {
|
||||
NearConstants[k] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package d2graph
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"oss.terrastruct.com/util-go/go2"
|
||||
)
|
||||
|
|
@ -48,7 +49,7 @@ func DeserializeGraph(bytes []byte, g *Graph) error {
|
|||
for _, id := range so["ChildrenArray"].([]interface{}) {
|
||||
o := idToObj[id.(string)]
|
||||
childrenArray = append(childrenArray, o)
|
||||
children[id.(string)] = o
|
||||
children[strings.ToLower(id.(string))] = o
|
||||
|
||||
o.Parent = idToObj[so["AbsID"].(string)]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,9 +14,7 @@ func TestSerialization(t *testing.T) {
|
|||
t.Parallel()
|
||||
|
||||
g, err := d2compiler.Compile("", strings.NewReader("a.a.b -> a.a.c"), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Nil(t, err)
|
||||
|
||||
asserts := func(g *d2graph.Graph) {
|
||||
assert.Equal(t, 4, len(g.Objects))
|
||||
|
|
@ -41,15 +39,33 @@ func TestSerialization(t *testing.T) {
|
|||
asserts(g)
|
||||
|
||||
b, err := d2graph.SerializeGraph(g)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Nil(t, err)
|
||||
|
||||
var newG d2graph.Graph
|
||||
err = d2graph.DeserializeGraph(b, &newG)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Nil(t, err)
|
||||
|
||||
asserts(&newG)
|
||||
}
|
||||
|
||||
func TestCasingRegression(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
script := `UserCreatedTypeField`
|
||||
|
||||
g, err := d2compiler.Compile("", strings.NewReader(script), nil)
|
||||
assert.Nil(t, err)
|
||||
|
||||
_, ok := g.Root.HasChild([]string{"UserCreatedTypeField"})
|
||||
assert.True(t, ok)
|
||||
|
||||
b, err := d2graph.SerializeGraph(g)
|
||||
assert.Nil(t, err)
|
||||
|
||||
var newG d2graph.Graph
|
||||
err = d2graph.DeserializeGraph(b, &newG)
|
||||
assert.Nil(t, err)
|
||||
|
||||
_, ok = newG.Root.HasChild([]string{"UserCreatedTypeField"})
|
||||
assert.True(t, ok)
|
||||
}
|
||||
|
|
|
|||
16
d2layouts/d2dagrelayout/dagre.js
vendored
|
|
@ -76,7 +76,7 @@ var lodash;if(typeof require==="function"){try{lodash={cloneDeep:require("lodash
|
|||
/*
|
||||
* A nesting graph creates dummy nodes for the tops and bottoms of subgraphs,
|
||||
* adds appropriate edges to ensure that all cluster nodes are placed between
|
||||
* these boundries, and ensures that the graph is connected.
|
||||
* these boundaries, and ensures that the graph is connected.
|
||||
*
|
||||
* In addition we ensure, through the use of the minlen property, that nodes
|
||||
* and subgraph border nodes to not end up on the same rank.
|
||||
|
|
@ -179,7 +179,7 @@ g.graph().nodeRankFactor=nodeSep}function dfs(g,root,nodeSep,weight,height,depth
|
|||
_.forEach(g[relationship](v),function(e){var u=e.v===v?e.w:e.v,edge=result.edge(u,v),weight=!_.isUndefined(edge)?edge.weight:0;result.setEdge(u,v,{weight:g.edge(e).weight+weight})});if(_.has(node,"minRank")){result.setNode(v,{borderLeft:node.borderLeft[rank],borderRight:node.borderRight[rank]})}}});return result}function createRootNode(g){var v;while(g.hasNode(v=_.uniqueId("_root")));return v}},{"../graphlib":7,"../lodash":10}],16:[function(require,module,exports){"use strict";var _=require("../lodash");module.exports=crossCount;
|
||||
/*
|
||||
* A function that takes a layering (an array of layers, each with an array of
|
||||
* ordererd nodes) and a graph and returns a weighted crossing count.
|
||||
* ordered nodes) and a graph and returns a weighted crossing count.
|
||||
*
|
||||
* Pre-conditions:
|
||||
*
|
||||
|
|
@ -232,11 +232,11 @@ var cc=0;_.forEach(southEntries.forEach(function(entry){var index=entry.pos+firs
|
|||
* constraint graph this function will resolve any conflicts between the
|
||||
* constraint graph and the barycenters for the entries. If the barycenters for
|
||||
* an entry would violate a constraint in the constraint graph then we coalesce
|
||||
* the nodes in the conflict into a new node that respects the contraint and
|
||||
* the nodes in the conflict into a new node that respects the constraint and
|
||||
* aggregates barycenter and weight information.
|
||||
*
|
||||
* This implementation is based on the description in Forster, "A Fast and
|
||||
* Simple Hueristic for Constrained Two-Level Crossing Reduction," thought it
|
||||
* Simple Heuristic for Constrained Two-Level Crossing Reduction," though it
|
||||
* differs in some specific details.
|
||||
*
|
||||
* Pre-conditions:
|
||||
|
|
@ -482,7 +482,7 @@ rank=0}return label.rank=rank}_.forEach(g.sources(),dfs)}
|
|||
* ({x, y, width, height}) if it were pointing at the rectangle's center.
|
||||
*/function intersectRect(rect,point){var x=rect.x;var y=rect.y;
|
||||
// Rectangle intersection algorithm from:
|
||||
// http://math.stackexchange.com/questions/108113/find-edge-between-two-boxes
|
||||
// https://math.stackexchange.com/questions/108113/find-edge-between-two-boxes
|
||||
var dx=point.x-x;var dy=point.y-y;var w=rect.width/2;var h=rect.height/2;if(!dx&&!dy){throw new Error("Not possible to find intersection inside of the rectangle")}var sx,sy;if(Math.abs(dy)*w>Math.abs(dx)*h){
|
||||
// Intersection is top or bottom of rect.
|
||||
if(dy<0){h=-h}sx=h*dx/dy;sy=h}else{
|
||||
|
|
@ -538,7 +538,7 @@ var offset=_.min(_.map(g.nodes(),function(v){return g.node(v).rank}));var layers
|
|||
*/
|
||||
var lib=require("./lib");module.exports={Graph:lib.Graph,json:require("./lib/json"),alg:require("./lib/alg"),version:lib.version}},{"./lib":47,"./lib/alg":38,"./lib/json":48}],32:[function(require,module,exports){var _=require("../lodash");module.exports=components;function components(g){var visited={};var cmpts=[];var cmpt;function dfs(v){if(_.has(visited,v))return;visited[v]=true;cmpt.push(v);_.each(g.successors(v),dfs);_.each(g.predecessors(v),dfs)}_.each(g.nodes(),function(v){cmpt=[];dfs(v);if(cmpt.length){cmpts.push(cmpt)}});return cmpts}},{"../lodash":49}],33:[function(require,module,exports){var _=require("../lodash");module.exports=dfs;
|
||||
/*
|
||||
* A helper that preforms a pre- or post-order traversal on the input graph
|
||||
* A helper that performs a pre- or post-order traversal on the input graph
|
||||
* and returns the nodes in the order they were visited. If the graph is
|
||||
* undirected then this algorithm will navigate using neighbors. If the graph
|
||||
* is directed then this algorithm will navigate using successors.
|
||||
|
|
@ -1114,7 +1114,7 @@ function baseHasIn(object,key){return object!=null&&key in Object(object)}module
|
|||
function baseIsNaN(value){return value!==value}module.exports=baseIsNaN},{}],102:[function(require,module,exports){var isFunction=require("./isFunction"),isMasked=require("./_isMasked"),isObject=require("./isObject"),toSource=require("./_toSource");
|
||||
/**
|
||||
* Used to match `RegExp`
|
||||
* [syntax characters](http://ecma-international.org/ecma-262/7.0/#sec-patterns).
|
||||
* [syntax characters](https://262.ecma-international.org/7.0/#sec-patterns).
|
||||
*/var reRegExpChar=/[\\^$.*+?()[\]{}|]/g;
|
||||
/** Used to detect host constructors (Safari). */var reIsHostCtor=/^\[object .+?Constructor\]$/;
|
||||
/** Used for built-in method references. */var funcProto=Function.prototype,objectProto=Object.prototype;
|
||||
|
|
@ -1718,7 +1718,7 @@ var freeGlobal=typeof global=="object"&&global&&global.Object===Object&&global;m
|
|||
/** Used to check objects for own properties. */var hasOwnProperty=objectProto.hasOwnProperty;
|
||||
/**
|
||||
* Used to resolve the
|
||||
* [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring)
|
||||
* [`toStringTag`](https://262.ecma-international.org/7.0/#sec-object.prototype.tostring)
|
||||
* of values.
|
||||
*/var nativeObjectToString=objectProto.toString;
|
||||
/** Built-in value references. */var symToStringTag=Symbol?Symbol.toStringTag:undefined;
|
||||
|
|
|
|||
|
|
@ -64,22 +64,35 @@ func Layout(ctx context.Context, g *d2graph.Graph) (err error) {
|
|||
}
|
||||
|
||||
rootAttrs := dagreGraphAttrs{
|
||||
ranksep: 100,
|
||||
edgesep: 40,
|
||||
nodesep: 60,
|
||||
}
|
||||
isHorizontal := false
|
||||
switch g.Root.Attributes.Direction.Value {
|
||||
case "down":
|
||||
rootAttrs.rankdir = "TB"
|
||||
case "right":
|
||||
rootAttrs.rankdir = "LR"
|
||||
isHorizontal = true
|
||||
case "left":
|
||||
rootAttrs.rankdir = "RL"
|
||||
isHorizontal = true
|
||||
case "up":
|
||||
rootAttrs.rankdir = "BT"
|
||||
default:
|
||||
rootAttrs.rankdir = "TB"
|
||||
}
|
||||
|
||||
maxLabelSize := 0
|
||||
for _, edge := range g.Edges {
|
||||
size := edge.LabelDimensions.Width
|
||||
if !isHorizontal {
|
||||
size = edge.LabelDimensions.Height
|
||||
}
|
||||
maxLabelSize = go2.Max(maxLabelSize, size)
|
||||
}
|
||||
rootAttrs.ranksep = go2.Max(100, maxLabelSize+40)
|
||||
|
||||
configJS := setGraphAttrs(rootAttrs)
|
||||
if _, err := vm.RunString(configJS); err != nil {
|
||||
return err
|
||||
|
|
@ -90,7 +103,14 @@ func Layout(ctx context.Context, g *d2graph.Graph) (err error) {
|
|||
for _, obj := range g.Objects {
|
||||
id := obj.AbsID()
|
||||
idToObj[id] = obj
|
||||
loadScript += generateAddNodeLine(id, int(obj.Width), int(obj.Height))
|
||||
|
||||
height := obj.Height
|
||||
if obj.LabelWidth != nil && obj.LabelHeight != nil {
|
||||
if obj.Attributes.Shape.Value == d2target.ShapeImage || obj.Attributes.Icon != nil {
|
||||
height += float64(*obj.LabelHeight) + label.PADDING
|
||||
}
|
||||
}
|
||||
loadScript += generateAddNodeLine(id, int(obj.Width), int(height))
|
||||
if obj.Parent != g.Root {
|
||||
loadScript += generateAddParentLine(id, obj.Parent.AbsID())
|
||||
}
|
||||
|
|
@ -151,8 +171,12 @@ func Layout(ctx context.Context, g *d2graph.Graph) (err error) {
|
|||
if obj.LabelWidth != nil && obj.LabelHeight != nil {
|
||||
if len(obj.ChildrenArray) > 0 {
|
||||
obj.LabelPosition = go2.Pointer(string(label.InsideTopCenter))
|
||||
} else if obj.Attributes.Shape.Value == d2target.ShapeImage || obj.Attributes.Icon != nil {
|
||||
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
|
||||
obj.Height -= float64(*obj.LabelHeight) + label.PADDING
|
||||
} else if obj.Attributes.Icon != nil {
|
||||
obj.LabelPosition = go2.Pointer(string(label.InsideTopCenter))
|
||||
} else {
|
||||
obj.LabelPosition = go2.Pointer(string(label.InsideMiddleCenter))
|
||||
}
|
||||
|
|
@ -261,7 +285,7 @@ func escapeID(id string) string {
|
|||
// fixes \\
|
||||
id = strings.ReplaceAll(id, "\\", `\\`)
|
||||
// replaces \n with \\n whenever \n is not preceded by \ (does not replace \\n)
|
||||
re := regexp.MustCompile(`[^\\](\n)`)
|
||||
re := regexp.MustCompile(`[^\\]\n`)
|
||||
id = re.ReplaceAllString(id, `\\n`)
|
||||
// avoid an unescaped \r becoming a \n in the layout result
|
||||
id = strings.ReplaceAll(id, "\r", `\r`)
|
||||
|
|
|
|||
|
|
@ -9,9 +9,10 @@ import (
|
|||
_ "embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
|
||||
"oss.terrastruct.com/util-go/xdefer"
|
||||
|
||||
"oss.terrastruct.com/util-go/go2"
|
||||
|
|
@ -20,6 +21,7 @@ import (
|
|||
"oss.terrastruct.com/d2/d2target"
|
||||
"oss.terrastruct.com/d2/lib/geo"
|
||||
"oss.terrastruct.com/d2/lib/label"
|
||||
"oss.terrastruct.com/d2/lib/shape"
|
||||
)
|
||||
|
||||
//go:embed elk.js
|
||||
|
|
@ -40,11 +42,12 @@ type ELKNode struct {
|
|||
}
|
||||
|
||||
type ELKLabel struct {
|
||||
Text string `json:"text"`
|
||||
X float64 `json:"x"`
|
||||
Y float64 `json:"y"`
|
||||
Width float64 `json:"width"`
|
||||
Height float64 `json:"height"`
|
||||
Text string `json:"text"`
|
||||
X float64 `json:"x"`
|
||||
Y float64 `json:"y"`
|
||||
Width float64 `json:"width"`
|
||||
Height float64 `json:"height"`
|
||||
LayoutOptions *ELKLayoutOptions `json:"layoutOptions,omitempty"`
|
||||
}
|
||||
|
||||
type ELKPoint struct {
|
||||
|
|
@ -75,13 +78,16 @@ type ELKGraph struct {
|
|||
}
|
||||
|
||||
type ELKLayoutOptions struct {
|
||||
Algorithm string `json:"elk.algorithm,omitempty"`
|
||||
HierarchyHandling string `json:"elk.hierarchyHandling,omitempty"`
|
||||
NodeSpacing float64 `json:"spacing.nodeNodeBetweenLayers,omitempty"`
|
||||
Padding string `json:"elk.padding,omitempty"`
|
||||
EdgeNodeSpacing float64 `json:"spacing.edgeNodeBetweenLayers,omitempty"`
|
||||
Direction string `json:"elk.direction"`
|
||||
SelfLoopSpacing float64 `json:"elk.spacing.nodeSelfLoop"`
|
||||
Algorithm string `json:"elk.algorithm,omitempty"`
|
||||
HierarchyHandling string `json:"elk.hierarchyHandling,omitempty"`
|
||||
NodeSpacing float64 `json:"spacing.nodeNodeBetweenLayers,omitempty"`
|
||||
Padding string `json:"elk.padding,omitempty"`
|
||||
EdgeNodeSpacing float64 `json:"spacing.edgeNodeBetweenLayers,omitempty"`
|
||||
Direction string `json:"elk.direction"`
|
||||
SelfLoopSpacing float64 `json:"elk.spacing.nodeSelfLoop"`
|
||||
InlineEdgeLabels bool `json:"elk.edgeLabels.inline,omitempty"`
|
||||
ConsiderModelOrder string `json:"elk.layered.considerModelOrder.strategy,omitempty"`
|
||||
ForceNodeModelOrder bool `json:"elk.layered.crossingMinimization.forceNodeModelOrder,omitempty"`
|
||||
}
|
||||
|
||||
func Layout(ctx context.Context, g *d2graph.Graph) (err error) {
|
||||
|
|
@ -104,11 +110,12 @@ func Layout(ctx context.Context, g *d2graph.Graph) (err error) {
|
|||
elkGraph := &ELKGraph{
|
||||
ID: "root",
|
||||
LayoutOptions: &ELKLayoutOptions{
|
||||
Algorithm: "layered",
|
||||
HierarchyHandling: "INCLUDE_CHILDREN",
|
||||
NodeSpacing: 100.0,
|
||||
EdgeNodeSpacing: 50.0,
|
||||
SelfLoopSpacing: 50.0,
|
||||
Algorithm: "layered",
|
||||
HierarchyHandling: "INCLUDE_CHILDREN",
|
||||
NodeSpacing: 100.0,
|
||||
EdgeNodeSpacing: 50.0,
|
||||
SelfLoopSpacing: 50.0,
|
||||
ConsiderModelOrder: "NODES_AND_EDGES",
|
||||
},
|
||||
}
|
||||
switch g.Root.Attributes.Direction.Value {
|
||||
|
|
@ -139,15 +146,23 @@ func Layout(ctx context.Context, g *d2graph.Graph) (err error) {
|
|||
}
|
||||
|
||||
walk(g.Root, nil, func(obj, parent *d2graph.Object) {
|
||||
height := obj.Height
|
||||
if obj.LabelWidth != nil && obj.LabelHeight != nil {
|
||||
if obj.Attributes.Shape.Value == d2target.ShapeImage || obj.Attributes.Icon != nil {
|
||||
height += float64(*obj.LabelHeight) + label.PADDING
|
||||
}
|
||||
}
|
||||
|
||||
n := &ELKNode{
|
||||
ID: obj.AbsID(),
|
||||
Width: obj.Width,
|
||||
Height: obj.Height,
|
||||
Height: height,
|
||||
}
|
||||
|
||||
if len(obj.ChildrenArray) > 0 {
|
||||
n.LayoutOptions = &ELKLayoutOptions{
|
||||
Padding: "[top=75,left=75,bottom=75,right=75]",
|
||||
Padding: "[top=75,left=75,bottom=75,right=75]",
|
||||
ForceNodeModelOrder: true,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -178,6 +193,9 @@ func Layout(ctx context.Context, g *d2graph.Graph) (err error) {
|
|||
Text: edge.Attributes.Label.Value,
|
||||
Width: float64(edge.LabelDimensions.Width),
|
||||
Height: float64(edge.LabelDimensions.Height),
|
||||
LayoutOptions: &ELKLayoutOptions{
|
||||
InlineEdgeLabels: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
elkGraph.Edges = append(elkGraph.Edges, e)
|
||||
|
|
@ -240,15 +258,18 @@ func Layout(ctx context.Context, g *d2graph.Graph) (err error) {
|
|||
parentX = parent.TopLeft.X
|
||||
parentY = parent.TopLeft.Y
|
||||
}
|
||||
obj.TopLeft = geo.NewPoint(math.Round(parentX+n.X), math.Round(parentY+n.Y))
|
||||
obj.TopLeft = geo.NewPoint(parentX+n.X, parentY+n.Y)
|
||||
obj.Width = n.Width
|
||||
obj.Height = n.Height
|
||||
|
||||
if obj.LabelWidth != nil && obj.LabelHeight != nil {
|
||||
if len(obj.ChildrenArray) > 0 {
|
||||
obj.LabelPosition = go2.Pointer(string(label.InsideTopCenter))
|
||||
} else if obj.Attributes.Shape.Value == d2target.ShapeImage || obj.Attributes.Icon != nil {
|
||||
obj.LabelPosition = go2.Pointer(string(label.OutsideTopCenter))
|
||||
} else if obj.Attributes.Shape.Value == d2target.ShapeImage {
|
||||
obj.LabelPosition = go2.Pointer(string(label.OutsideBottomCenter))
|
||||
obj.Height -= float64(*obj.LabelHeight) + label.PADDING
|
||||
} else if obj.Attributes.Icon != nil {
|
||||
obj.LabelPosition = go2.Pointer(string(label.InsideTopCenter))
|
||||
} else {
|
||||
obj.LabelPosition = go2.Pointer(string(label.InsideMiddleCenter))
|
||||
}
|
||||
|
|
@ -288,6 +309,14 @@ func Layout(ctx context.Context, g *d2graph.Graph) (err error) {
|
|||
})
|
||||
}
|
||||
|
||||
startIndex, endIndex := 0, len(points)-1
|
||||
srcShape := shape.NewShape(d2target.DSL_SHAPE_TO_SHAPE_TYPE[strings.ToLower(edge.Src.Attributes.Shape.Value)], edge.Src.Box)
|
||||
dstShape := shape.NewShape(d2target.DSL_SHAPE_TO_SHAPE_TYPE[strings.ToLower(edge.Dst.Attributes.Shape.Value)], edge.Dst.Box)
|
||||
|
||||
// trace the edge to the specific shape's border
|
||||
points[startIndex] = shape.TraceToShapeBorder(srcShape, points[startIndex], points[startIndex+1])
|
||||
points[endIndex] = shape.TraceToShapeBorder(dstShape, points[endIndex], points[endIndex-1])
|
||||
|
||||
if edge.Attributes.Label.Value != "" {
|
||||
edge.LabelPosition = go2.Pointer(string(label.InsideMiddleCenter))
|
||||
}
|
||||
|
|
|
|||
146
d2layouts/d2near/layout.go
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
// d2near applies near keywords when they're constants
|
||||
// Intended to be run as the last stage of layout after the diagram has already undergone layout
|
||||
package d2near
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math"
|
||||
"strings"
|
||||
|
||||
"oss.terrastruct.com/d2/d2graph"
|
||||
"oss.terrastruct.com/d2/d2target"
|
||||
"oss.terrastruct.com/d2/lib/geo"
|
||||
"oss.terrastruct.com/d2/lib/label"
|
||||
"oss.terrastruct.com/util-go/go2"
|
||||
)
|
||||
|
||||
const pad = 20
|
||||
|
||||
// Layout finds the shapes which are assigned constant near keywords and places them.
|
||||
func Layout(ctx context.Context, g *d2graph.Graph, constantNears []*d2graph.Object) error {
|
||||
if len(constantNears) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Imagine the graph has two long texts, one at top center and one at top left.
|
||||
// Top left should go left enough to not collide with center.
|
||||
// So place the center ones first, then the later ones will consider them for bounding box
|
||||
for _, processCenters := range []bool{true, false} {
|
||||
for _, obj := range constantNears {
|
||||
if processCenters == strings.Contains(d2graph.Key(obj.Attributes.NearKey)[0], "center") {
|
||||
obj.TopLeft = geo.NewPoint(place(obj))
|
||||
}
|
||||
}
|
||||
for _, obj := range constantNears {
|
||||
if processCenters == strings.Contains(d2graph.Key(obj.Attributes.NearKey)[0], "center") {
|
||||
// The z-index for constant nears does not matter, as it will not collide
|
||||
g.Objects = append(g.Objects, obj)
|
||||
obj.Parent.Children[obj.ID] = obj
|
||||
obj.Parent.ChildrenArray = append(obj.Parent.ChildrenArray, obj)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// These shapes skipped core layout, which means they also skipped label placements
|
||||
for _, obj := range constantNears {
|
||||
if obj.Attributes.Shape.Value == d2target.ShapeImage {
|
||||
obj.LabelPosition = go2.Pointer(string(label.OutsideBottomCenter))
|
||||
} else if obj.Attributes.Icon != nil {
|
||||
obj.LabelPosition = go2.Pointer(string(label.InsideTopCenter))
|
||||
} else {
|
||||
obj.LabelPosition = go2.Pointer(string(label.InsideMiddleCenter))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// place returns the position of obj, taking into consideration its near value and the diagram
|
||||
func place(obj *d2graph.Object) (float64, float64) {
|
||||
tl, br := boundingBox(obj.Graph)
|
||||
w := br.X - tl.X
|
||||
h := br.Y - tl.Y
|
||||
switch d2graph.Key(obj.Attributes.NearKey)[0] {
|
||||
case "top-left":
|
||||
return tl.X - obj.Width - pad, tl.Y - obj.Height - pad
|
||||
case "top-center":
|
||||
return tl.X + w/2 - obj.Width/2, tl.Y - obj.Height - pad
|
||||
case "top-right":
|
||||
return br.X + pad, tl.Y - obj.Height - pad
|
||||
case "center-left":
|
||||
return tl.X - obj.Width - pad, tl.Y + h/2 - obj.Height/2
|
||||
case "center-right":
|
||||
return br.X + pad, tl.Y + h/2 - obj.Height/2
|
||||
case "bottom-left":
|
||||
return tl.X - obj.Width - pad, br.Y + pad
|
||||
case "bottom-center":
|
||||
return br.X - w/2 - obj.Width/2, br.Y + pad
|
||||
case "bottom-right":
|
||||
return br.X + pad, br.Y + pad
|
||||
}
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
// WithoutConstantNears plucks out the graph objects which have "near" set to a constant value
|
||||
// This is to be called before layout engines so they don't take part in regular positioning
|
||||
func WithoutConstantNears(ctx context.Context, g *d2graph.Graph) (nears []*d2graph.Object) {
|
||||
for i := 0; i < len(g.Objects); i++ {
|
||||
obj := g.Objects[i]
|
||||
if obj.Attributes.NearKey == nil {
|
||||
continue
|
||||
}
|
||||
_, isKey := g.Root.HasChild(d2graph.Key(obj.Attributes.NearKey))
|
||||
if isKey {
|
||||
continue
|
||||
}
|
||||
_, isConst := d2graph.NearConstants[d2graph.Key(obj.Attributes.NearKey)[0]]
|
||||
if isConst {
|
||||
nears = append(nears, obj)
|
||||
g.Objects = append(g.Objects[:i], g.Objects[i+1:]...)
|
||||
i--
|
||||
delete(obj.Parent.Children, 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:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nears
|
||||
}
|
||||
|
||||
// boundingBox gets the center of the graph as defined by shapes
|
||||
// The bounds taking into consideration only shapes gives more of a feeling of true center
|
||||
// It differs from d2target.BoundingBox which needs to include every visible thing
|
||||
func boundingBox(g *d2graph.Graph) (tl, br *geo.Point) {
|
||||
if len(g.Objects) == 0 {
|
||||
return geo.NewPoint(0, 0), geo.NewPoint(0, 0)
|
||||
}
|
||||
x1 := math.Inf(1)
|
||||
y1 := math.Inf(1)
|
||||
x2 := math.Inf(-1)
|
||||
y2 := math.Inf(-1)
|
||||
|
||||
for _, obj := range g.Objects {
|
||||
if obj.Attributes.NearKey != nil {
|
||||
// Top left should not be MORE top than top-center
|
||||
// But it should go more left if top-center label extends beyond bounds of diagram
|
||||
switch d2graph.Key(obj.Attributes.NearKey)[0] {
|
||||
case "top-center", "bottom-center":
|
||||
x1 = math.Min(x1, obj.TopLeft.X)
|
||||
x2 = math.Max(x2, obj.TopLeft.X+obj.Width)
|
||||
case "center-left", "center-right":
|
||||
y1 = math.Min(y1, obj.TopLeft.Y)
|
||||
y2 = math.Max(y2, obj.TopLeft.Y+obj.Height)
|
||||
}
|
||||
} else {
|
||||
x1 = math.Min(x1, obj.TopLeft.X)
|
||||
y1 = math.Min(y1, obj.TopLeft.Y)
|
||||
x2 = math.Max(x2, obj.TopLeft.X+obj.Width)
|
||||
y2 = math.Max(y2, obj.TopLeft.Y+obj.Height)
|
||||
}
|
||||
}
|
||||
|
||||
return geo.NewPoint(x1, y1), geo.NewPoint(x2, y2)
|
||||
}
|
||||
|
|
@ -5,24 +5,14 @@ import (
|
|||
"sort"
|
||||
"strings"
|
||||
|
||||
"oss.terrastruct.com/util-go/go2"
|
||||
|
||||
"oss.terrastruct.com/d2/d2graph"
|
||||
"oss.terrastruct.com/d2/d2target"
|
||||
"oss.terrastruct.com/d2/lib/geo"
|
||||
"oss.terrastruct.com/d2/lib/label"
|
||||
"oss.terrastruct.com/util-go/go2"
|
||||
)
|
||||
|
||||
// Layout runs the sequence diagram layout engine on objects of shape sequence_diagram
|
||||
//
|
||||
// 1. Traverse graph from root, skip objects with shape not `sequence_diagram`
|
||||
// 2. Construct a sequence diagram from all descendant objects and edges
|
||||
// 3. Remove those objects and edges from the main graph
|
||||
// 4. Run layout on sequence diagrams
|
||||
// 5. Set the resulting dimensions to the main graph shape
|
||||
// 6. Run core layouts (still without sequence diagram innards)
|
||||
// 7. Put back sequence diagram innards in correct location
|
||||
func Layout(ctx context.Context, g *d2graph.Graph, layout func(ctx context.Context, g *d2graph.Graph) error) error {
|
||||
func WithoutSequenceDiagrams(ctx context.Context, g *d2graph.Graph) (map[string]*sequenceDiagram, map[string]int, map[string]int, error) {
|
||||
objectsToRemove := make(map[*d2graph.Object]struct{})
|
||||
edgesToRemove := make(map[*d2graph.Edge]struct{})
|
||||
sequenceDiagrams := make(map[string]*sequenceDiagram)
|
||||
|
|
@ -42,7 +32,7 @@ func Layout(ctx context.Context, g *d2graph.Graph, layout func(ctx context.Conte
|
|||
|
||||
sd, err := layoutSequenceDiagram(g, obj)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
obj.Children = make(map[string]*d2graph.Object)
|
||||
obj.ChildrenArray = nil
|
||||
|
|
@ -70,8 +60,27 @@ func Layout(ctx context.Context, g *d2graph.Graph, layout func(ctx context.Conte
|
|||
layoutEdges, edgeOrder := getLayoutEdges(g, edgesToRemove)
|
||||
g.Edges = layoutEdges
|
||||
layoutObjects, objectOrder := getLayoutObjects(g, objectsToRemove)
|
||||
// TODO this isn't a proper deletion because the objects still appear as children of the object
|
||||
g.Objects = layoutObjects
|
||||
|
||||
return sequenceDiagrams, objectOrder, edgeOrder, nil
|
||||
}
|
||||
|
||||
// Layout runs the sequence diagram layout engine on objects of shape sequence_diagram
|
||||
//
|
||||
// 1. Traverse graph from root, skip objects with shape not `sequence_diagram`
|
||||
// 2. Construct a sequence diagram from all descendant objects and edges
|
||||
// 3. Remove those objects and edges from the main graph
|
||||
// 4. Run layout on sequence diagrams
|
||||
// 5. Set the resulting dimensions to the main graph shape
|
||||
// 6. Run core layouts (still without sequence diagram innards)
|
||||
// 7. Put back sequence diagram innards in correct location
|
||||
func Layout(ctx context.Context, g *d2graph.Graph, layout func(ctx context.Context, g *d2graph.Graph) error) error {
|
||||
sequenceDiagrams, objectOrder, edgeOrder, err := WithoutSequenceDiagrams(ctx, g)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if g.Root.IsSequenceDiagram() {
|
||||
// the sequence diagram is the only layout engine if the whole diagram is
|
||||
// shape: sequence_diagram
|
||||
|
|
@ -89,7 +98,7 @@ func layoutSequenceDiagram(g *d2graph.Graph, obj *d2graph.Object) (*sequenceDiag
|
|||
var edges []*d2graph.Edge
|
||||
for _, edge := range g.Edges {
|
||||
// both Src and Dst must be inside the sequence diagram
|
||||
if strings.HasPrefix(edge.Src.AbsID(), obj.AbsID()) && strings.HasPrefix(edge.Dst.AbsID(), obj.AbsID()) {
|
||||
if obj == g.Root || (strings.HasPrefix(edge.Src.AbsID(), obj.AbsID()+".") && strings.HasPrefix(edge.Dst.AbsID(), obj.AbsID()+".")) {
|
||||
edges = append(edges, edge)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,6 +74,7 @@ func newSequenceDiagram(objects []*d2graph.Object, messages []*d2graph.Edge) *se
|
|||
// Groups may have more nested groups
|
||||
for len(queue) > 0 {
|
||||
curr := queue[0]
|
||||
curr.LabelPosition = go2.Pointer(string(label.InsideTopLeft))
|
||||
groups = append(groups, curr)
|
||||
queue = queue[1:]
|
||||
queue = append(queue, curr.ChildrenArray...)
|
||||
|
|
@ -299,14 +300,19 @@ func (sd *sequenceDiagram) placeActors() {
|
|||
// │
|
||||
// │
|
||||
func (sd *sequenceDiagram) addLifelineEdges() {
|
||||
lastRoute := sd.messages[len(sd.messages)-1].Route
|
||||
endY := 0.
|
||||
for _, p := range lastRoute {
|
||||
endY = math.Max(endY, p.Y)
|
||||
if len(sd.messages) > 0 {
|
||||
lastRoute := sd.messages[len(sd.messages)-1].Route
|
||||
for _, p := range lastRoute {
|
||||
endY = math.Max(endY, p.Y)
|
||||
}
|
||||
}
|
||||
for _, note := range sd.notes {
|
||||
endY = math.Max(endY, note.TopLeft.Y+note.Height)
|
||||
}
|
||||
for _, actor := range sd.actors {
|
||||
endY = math.Max(endY, actor.TopLeft.Y+actor.Height)
|
||||
}
|
||||
endY += sd.yStep
|
||||
|
||||
for _, actor := range sd.actors {
|
||||
|
|
@ -461,8 +467,8 @@ func (sd *sequenceDiagram) routeMessages() error {
|
|||
} else {
|
||||
return fmt.Errorf("could not find center of %s", message.Dst.AbsID())
|
||||
}
|
||||
isToDescendant := strings.HasPrefix(message.Dst.AbsID(), message.Src.AbsID())
|
||||
isFromDescendant := strings.HasPrefix(message.Src.AbsID(), message.Dst.AbsID())
|
||||
isToDescendant := strings.HasPrefix(message.Dst.AbsID(), message.Src.AbsID()+".")
|
||||
isFromDescendant := strings.HasPrefix(message.Src.AbsID(), message.Dst.AbsID()+".")
|
||||
isSelfMessage := message.Src == message.Dst
|
||||
|
||||
if isSelfMessage || isToDescendant || isFromDescendant {
|
||||
|
|
|
|||
29
d2lib/d2.go
|
|
@ -9,7 +9,9 @@ import (
|
|||
"oss.terrastruct.com/d2/d2compiler"
|
||||
"oss.terrastruct.com/d2/d2exporter"
|
||||
"oss.terrastruct.com/d2/d2graph"
|
||||
"oss.terrastruct.com/d2/d2layouts/d2near"
|
||||
"oss.terrastruct.com/d2/d2layouts/d2sequence"
|
||||
"oss.terrastruct.com/d2/d2renderers/d2fonts"
|
||||
"oss.terrastruct.com/d2/d2target"
|
||||
"oss.terrastruct.com/d2/lib/textmeasure"
|
||||
)
|
||||
|
|
@ -20,7 +22,13 @@ type CompileOptions struct {
|
|||
Ruler *textmeasure.Ruler
|
||||
Layout func(context.Context, *d2graph.Graph) error
|
||||
|
||||
ThemeID int64
|
||||
// FontFamily controls the font family used for all texts that are not the following:
|
||||
// - code
|
||||
// - latex
|
||||
// - 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) {
|
||||
|
|
@ -36,19 +44,30 @@ func Compile(ctx context.Context, input string, opts *CompileOptions) (*d2target
|
|||
}
|
||||
|
||||
if len(g.Objects) > 0 {
|
||||
err = g.SetDimensions(opts.MeasuredTexts, opts.Ruler)
|
||||
err = g.SetDimensions(opts.MeasuredTexts, opts.Ruler, opts.FontFamily)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if layout, err := getLayout(opts); err != nil {
|
||||
coreLayout, err := getLayout(opts)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
} else if err := d2sequence.Layout(ctx, g, layout); err != nil {
|
||||
}
|
||||
|
||||
constantNears := d2near.WithoutConstantNears(ctx, g)
|
||||
|
||||
err = d2sequence.Layout(ctx, g, coreLayout)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
err = d2near.Layout(ctx, g, constantNears)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
diagram, err := d2exporter.Export(ctx, g, opts.ThemeID)
|
||||
diagram, err := d2exporter.Export(ctx, g, opts.ThemeID, opts.FontFamily)
|
||||
return diagram, g, err
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,9 +8,9 @@ import (
|
|||
"io"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
"oss.terrastruct.com/util-go/xmain"
|
||||
|
||||
"oss.terrastruct.com/d2/d2graph"
|
||||
"oss.terrastruct.com/util-go/xmain"
|
||||
)
|
||||
|
||||
// Serve returns a xmain.RunFunc that will invoke the plugin p as necessary to service the
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// d2fonts holds fonts for renderings
|
||||
|
||||
// TODO write a script to do this as part of CI
|
||||
// Currently using an online converter: https://dopiaza.org/tools/datauri/index.php
|
||||
package d2fonts
|
||||
|
||||
import (
|
||||
|
|
@ -8,7 +9,7 @@ import (
|
|||
"strings"
|
||||
)
|
||||
|
||||
type FontFamily int
|
||||
type FontFamily string
|
||||
type FontStyle string
|
||||
|
||||
type Font struct {
|
||||
|
|
@ -38,8 +39,9 @@ const (
|
|||
FONT_STYLE_BOLD FontStyle = "bold"
|
||||
FONT_STYLE_ITALIC FontStyle = "italic"
|
||||
|
||||
SourceSansPro FontFamily = iota
|
||||
SourceCodePro FontFamily = iota
|
||||
SourceSansPro FontFamily = "SourceSansPro"
|
||||
SourceCodePro FontFamily = "SourceCodePro"
|
||||
HandDrawn FontFamily = "HandDrawn"
|
||||
)
|
||||
|
||||
var FontSizes = []int{
|
||||
|
|
@ -61,6 +63,7 @@ var FontStyles = []FontStyle{
|
|||
var FontFamilies = []FontFamily{
|
||||
SourceSansPro,
|
||||
SourceCodePro,
|
||||
HandDrawn,
|
||||
}
|
||||
|
||||
//go:embed encoded/SourceSansPro-Regular.txt
|
||||
|
|
@ -75,6 +78,12 @@ var sourceSansProItalicBase64 string
|
|||
//go:embed encoded/SourceCodePro-Regular.txt
|
||||
var sourceCodeProRegularBase64 string
|
||||
|
||||
//go:embed encoded/ArchitectsDaughter-Regular.txt
|
||||
var architectsDaughterRegularBase64 string
|
||||
|
||||
//go:embed encoded/FuzzyBubbles-Bold.txt
|
||||
var fuzzyBubblesBoldBase64 string
|
||||
|
||||
//go:embed ttf/*
|
||||
var fontFacesFS embed.FS
|
||||
|
||||
|
|
@ -99,6 +108,19 @@ func init() {
|
|||
Family: SourceCodePro,
|
||||
Style: FONT_STYLE_REGULAR,
|
||||
}: sourceCodeProRegularBase64,
|
||||
{
|
||||
Family: HandDrawn,
|
||||
Style: FONT_STYLE_REGULAR,
|
||||
}: architectsDaughterRegularBase64,
|
||||
{
|
||||
Family: HandDrawn,
|
||||
Style: FONT_STYLE_ITALIC,
|
||||
// This font has no italic, so just reuse regular
|
||||
}: architectsDaughterRegularBase64,
|
||||
{
|
||||
Family: HandDrawn,
|
||||
Style: FONT_STYLE_BOLD,
|
||||
}: fuzzyBubblesBoldBase64,
|
||||
}
|
||||
|
||||
for k, v := range FontEncodings {
|
||||
|
|
@ -138,4 +160,24 @@ func init() {
|
|||
Family: SourceSansPro,
|
||||
Style: FONT_STYLE_ITALIC,
|
||||
}] = b
|
||||
b, err = fontFacesFS.ReadFile("ttf/ArchitectsDaughter-Regular.ttf")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
FontFaces[Font{
|
||||
Family: HandDrawn,
|
||||
Style: FONT_STYLE_REGULAR,
|
||||
}] = b
|
||||
FontFaces[Font{
|
||||
Family: HandDrawn,
|
||||
Style: FONT_STYLE_ITALIC,
|
||||
}] = b
|
||||
b, err = fontFacesFS.ReadFile("ttf/FuzzyBubbles-Bold.ttf")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
FontFaces[Font{
|
||||
Family: HandDrawn,
|
||||
Style: FONT_STYLE_BOLD,
|
||||
}] = b
|
||||
}
|
||||
|
|
|
|||
1
d2renderers/d2fonts/encoded/FuzzyBubbles-Bold.txt
Normal file
BIN
d2renderers/d2fonts/ttf/ArchitectsDaughter-Regular.ttf
Normal file
BIN
d2renderers/d2fonts/ttf/FuzzyBubbles-Bold.ttf
Normal file
|
|
@ -8,6 +8,7 @@ import (
|
|||
"strconv"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
|
||||
"oss.terrastruct.com/util-go/xdefer"
|
||||
)
|
||||
|
||||
|
|
|
|||
1
d2renderers/d2sketch/fillpattern.svg
Normal file
1673
d2renderers/d2sketch/rough.js
Normal file
19
d2renderers/d2sketch/setup.js
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
const root = {
|
||||
ownerDocument: {
|
||||
createElementNS: (ns, tagName) => {
|
||||
const children = [];
|
||||
const attrs = {};
|
||||
const style = {};
|
||||
return {
|
||||
style,
|
||||
tagName,
|
||||
attrs,
|
||||
setAttribute: (key, value) => (attrs[key] = value),
|
||||
appendChild: (node) => children.push(node),
|
||||
children,
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
const rc = rough.svg(root, { seed: 1 });
|
||||
let node;
|
||||
498
d2renderers/d2sketch/sketch.go
Normal file
|
|
@ -0,0 +1,498 @@
|
|||
package d2sketch
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
_ "embed"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
|
||||
"oss.terrastruct.com/d2/d2target"
|
||||
"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
|
||||
|
||||
type Runner goja.Runtime
|
||||
|
||||
var baseRoughProps = `fillWeight: 2.0,
|
||||
hachureGap: 16,
|
||||
fillStyle: "solid",
|
||||
bowing: 2,
|
||||
seed: 1,`
|
||||
|
||||
func (r *Runner) run(js string) (goja.Value, error) {
|
||||
vm := (*goja.Runtime)(r)
|
||||
return vm.RunString(js)
|
||||
}
|
||||
|
||||
func InitSketchVM() (*Runner, error) {
|
||||
vm := goja.New()
|
||||
if _, err := vm.RunString(roughJS); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := vm.RunString(setupJS); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r := Runner(*vm)
|
||||
return &r, nil
|
||||
}
|
||||
|
||||
// DefineFillPattern adds a reusable pattern that is 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 shapeStyle(shape d2target.Shape) string {
|
||||
out := ""
|
||||
|
||||
if shape.Type == d2target.ShapeSQLTable || shape.Type == d2target.ShapeClass {
|
||||
out += fmt.Sprintf(`fill:%s;`, shape.Stroke)
|
||||
out += fmt.Sprintf(`stroke:%s;`, shape.Fill)
|
||||
} else {
|
||||
out += fmt.Sprintf(`fill:%s;`, shape.Fill)
|
||||
out += fmt.Sprintf(`stroke:%s;`, shape.Stroke)
|
||||
}
|
||||
out += fmt.Sprintf(`opacity:%f;`, shape.Opacity)
|
||||
out += fmt.Sprintf(`stroke-width:%d;`, shape.StrokeWidth)
|
||||
if shape.StrokeDash != 0 {
|
||||
dashSize, gapSize := svg.GetStrokeDashAttributes(float64(shape.StrokeWidth), shape.StrokeDash)
|
||||
out += fmt.Sprintf(`stroke-dasharray:%f,%f;`, dashSize, gapSize)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func Rect(r *Runner, shape d2target.Shape) (string, error) {
|
||||
js := fmt.Sprintf(`node = rc.rectangle(0, 0, %d, %d, {
|
||||
fill: "%s",
|
||||
stroke: "%s",
|
||||
strokeWidth: %d,
|
||||
%s
|
||||
});`, shape.Width, shape.Height, shape.Fill, shape.Stroke, shape.StrokeWidth, baseRoughProps)
|
||||
paths, err := computeRoughPaths(r, js)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
output := ""
|
||||
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, shapeStyle(shape),
|
||||
)
|
||||
}
|
||||
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,
|
||||
)
|
||||
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",
|
||||
strokeWidth: %d,
|
||||
%s
|
||||
});`, shape.Width/2, shape.Height/2, shape.Width, shape.Height, shape.Fill, shape.Stroke, shape.StrokeWidth, baseRoughProps)
|
||||
paths, err := computeRoughPaths(r, js)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
output := ""
|
||||
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, shapeStyle(shape),
|
||||
)
|
||||
}
|
||||
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,
|
||||
)
|
||||
return output, nil
|
||||
}
|
||||
|
||||
// TODO need to personalize this per shape like we do in Terrastruct app
|
||||
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",
|
||||
strokeWidth: %d,
|
||||
%s
|
||||
});`, path, shape.Fill, shape.Stroke, shape.StrokeWidth, baseRoughProps)
|
||||
sketchPaths, err := computeRoughPaths(r, js)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for _, p := range sketchPaths {
|
||||
output += fmt.Sprintf(
|
||||
`<path class="shape" d="%s" style="%s" />`,
|
||||
p, shapeStyle(shape),
|
||||
)
|
||||
}
|
||||
for _, p := range sketchPaths {
|
||||
output += fmt.Sprintf(
|
||||
`<path class="sketch-overlay" d="%s" />`,
|
||||
p,
|
||||
)
|
||||
}
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func connectionStyle(connection d2target.Connection) string {
|
||||
out := ""
|
||||
|
||||
out += fmt.Sprintf(`stroke:%s;`, connection.Stroke)
|
||||
out += fmt.Sprintf(`opacity:%f;`, connection.Opacity)
|
||||
out += fmt.Sprintf(`stroke-width:%d;`, connection.StrokeWidth)
|
||||
if connection.StrokeDash != 0 {
|
||||
dashSize, gapSize := svg.GetStrokeDashAttributes(float64(connection.StrokeWidth), connection.StrokeDash)
|
||||
out += fmt.Sprintf(`stroke-dasharray:%f,%f;`, dashSize, gapSize)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func Connection(r *Runner, connection d2target.Connection, path, attrs string) (string, error) {
|
||||
roughness := 1.0
|
||||
js := fmt.Sprintf(`node = rc.path("%s", {roughness: %f, seed: 1});`, path, roughness)
|
||||
paths, err := computeRoughPaths(r, js)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
output := ""
|
||||
for _, p := range paths {
|
||||
output += fmt.Sprintf(
|
||||
`<path class="connection" fill="none" d="%s" style="%s" %s/>`,
|
||||
p, connectionStyle(connection), attrs,
|
||||
)
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
|
||||
// TODO cleanup
|
||||
func Table(r *Runner, shape d2target.Shape) (string, error) {
|
||||
output := ""
|
||||
js := fmt.Sprintf(`node = rc.rectangle(0, 0, %d, %d, {
|
||||
fill: "%s",
|
||||
stroke: "%s",
|
||||
strokeWidth: %d,
|
||||
%s
|
||||
});`, shape.Width, shape.Height, shape.Fill, shape.Stroke, shape.StrokeWidth, baseRoughProps)
|
||||
paths, err := computeRoughPaths(r, js)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
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, shapeStyle(shape),
|
||||
)
|
||||
}
|
||||
|
||||
box := geo.NewBox(
|
||||
geo.NewPoint(float64(shape.Pos.X), float64(shape.Pos.Y)),
|
||||
float64(shape.Width),
|
||||
float64(shape.Height),
|
||||
)
|
||||
rowHeight := box.Height / float64(1+len(shape.SQLTable.Columns))
|
||||
headerBox := geo.NewBox(box.TopLeft, box.Width, rowHeight)
|
||||
|
||||
js = fmt.Sprintf(`node = rc.rectangle(0, 0, %d, %f, {
|
||||
fill: "%s",
|
||||
%s
|
||||
});`, shape.Width, rowHeight, shape.Fill, baseRoughProps)
|
||||
paths, err = computeRoughPaths(r, js)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
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,
|
||||
)
|
||||
}
|
||||
|
||||
if shape.Label != "" {
|
||||
tl := label.InsideMiddleLeft.GetPointOnBox(
|
||||
headerBox,
|
||||
20,
|
||||
float64(shape.LabelWidth),
|
||||
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),
|
||||
)
|
||||
}
|
||||
|
||||
var longestNameWidth int
|
||||
for _, f := range shape.Columns {
|
||||
longestNameWidth = go2.Max(longestNameWidth, f.Name.LabelWidth)
|
||||
}
|
||||
|
||||
rowBox := geo.NewBox(box.TopLeft.Copy(), box.Width, rowHeight)
|
||||
rowBox.TopLeft.Y += headerBox.Height
|
||||
for _, f := range shape.Columns {
|
||||
nameTL := label.InsideMiddleLeft.GetPointOnBox(
|
||||
rowBox,
|
||||
d2target.NamePadding,
|
||||
rowBox.Width,
|
||||
float64(shape.FontSize),
|
||||
)
|
||||
constraintTR := label.InsideMiddleRight.GetPointOnBox(
|
||||
rowBox,
|
||||
d2target.TypePadding,
|
||||
0,
|
||||
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")
|
||||
|
||||
rowBox.TopLeft.Y += rowHeight
|
||||
|
||||
js = fmt.Sprintf(`node = rc.line(%f, %f, %f, %f, {
|
||||
%s
|
||||
});`, rowBox.TopLeft.X, rowBox.TopLeft.Y, rowBox.TopLeft.X+rowBox.Width, rowBox.TopLeft.Y, baseRoughProps)
|
||||
paths, err = computeRoughPaths(r, js)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for _, p := range paths {
|
||||
output += fmt.Sprintf(
|
||||
`<path d="%s" style="fill:%s" />`,
|
||||
p, shape.Fill,
|
||||
)
|
||||
}
|
||||
}
|
||||
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,
|
||||
)
|
||||
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",
|
||||
strokeWidth: %d,
|
||||
%s
|
||||
});`, shape.Width, shape.Height, shape.Fill, shape.Stroke, shape.StrokeWidth, baseRoughProps)
|
||||
paths, err := computeRoughPaths(r, js)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
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, shapeStyle(shape),
|
||||
)
|
||||
}
|
||||
|
||||
box := geo.NewBox(
|
||||
geo.NewPoint(float64(shape.Pos.X), float64(shape.Pos.Y)),
|
||||
float64(shape.Width),
|
||||
float64(shape.Height),
|
||||
)
|
||||
|
||||
rowHeight := box.Height / float64(2+len(shape.Class.Fields)+len(shape.Class.Methods))
|
||||
headerBox := geo.NewBox(box.TopLeft, box.Width, 2*rowHeight)
|
||||
|
||||
js = fmt.Sprintf(`node = rc.rectangle(0, 0, %d, %f, {
|
||||
fill: "%s",
|
||||
%s
|
||||
});`, shape.Width, headerBox.Height, shape.Fill, baseRoughProps)
|
||||
paths, err = computeRoughPaths(r, js)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
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,
|
||||
)
|
||||
}
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
if shape.Label != "" {
|
||||
tl := label.InsideMiddleCenter.GetPointOnBox(
|
||||
headerBox,
|
||||
0,
|
||||
float64(shape.LabelWidth),
|
||||
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),
|
||||
)
|
||||
}
|
||||
|
||||
rowBox := geo.NewBox(box.TopLeft.Copy(), box.Width, rowHeight)
|
||||
rowBox.TopLeft.Y += headerBox.Height
|
||||
for _, f := range shape.Fields {
|
||||
output += classRow(shape, rowBox, f.VisibilityToken(), f.Name, f.Type, float64(shape.FontSize))
|
||||
rowBox.TopLeft.Y += rowHeight
|
||||
}
|
||||
|
||||
js = fmt.Sprintf(`node = rc.line(%f, %f, %f, %f, {
|
||||
%s
|
||||
});`, rowBox.TopLeft.X, rowBox.TopLeft.Y, rowBox.TopLeft.X+rowBox.Width, rowBox.TopLeft.Y, baseRoughProps)
|
||||
paths, err = computeRoughPaths(r, js)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for _, p := range paths {
|
||||
output += fmt.Sprintf(
|
||||
`<path class="class_header" d="%s" style="fill:%s" />`,
|
||||
p, shape.Fill,
|
||||
)
|
||||
}
|
||||
|
||||
for _, m := range shape.Methods {
|
||||
output += classRow(shape, rowBox, m.VisibilityToken(), m.Name, m.Return, float64(shape.FontSize))
|
||||
rowBox.TopLeft.Y += rowHeight
|
||||
}
|
||||
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func classRow(shape d2target.Shape, box *geo.Box, prefix, nameText, typeText string, fontSize float64) string {
|
||||
output := ""
|
||||
prefixTL := label.InsideMiddleLeft.GetPointOnBox(
|
||||
box,
|
||||
d2target.PrefixPadding,
|
||||
box.Width,
|
||||
fontSize,
|
||||
)
|
||||
typeTR := label.InsideMiddleRight.GetPointOnBox(
|
||||
box,
|
||||
d2target.TypePadding,
|
||||
0,
|
||||
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,
|
||||
),
|
||||
|
||||
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),
|
||||
),
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func computeRoughPaths(r *Runner, js string) ([]string, error) {
|
||||
if _, err := r.run(js); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return extractPaths(r)
|
||||
}
|
||||
|
||||
type attrs struct {
|
||||
D string `json:"d"`
|
||||
}
|
||||
|
||||
type node struct {
|
||||
Attrs attrs `json:"attrs"`
|
||||
}
|
||||
|
||||
func extractPaths(r *Runner) ([]string, error) {
|
||||
val, err := r.run("JSON.stringify(node.children)")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var nodes []node
|
||||
|
||||
err = json.Unmarshal([]byte(val.String()), &nodes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var paths []string
|
||||
for _, n := range nodes {
|
||||
paths = append(paths, n.Attrs.D)
|
||||
}
|
||||
|
||||
return paths, nil
|
||||
}
|
||||
344
d2renderers/d2sketch/sketch_test.go
Normal file
|
|
@ -0,0 +1,344 @@
|
|||
package d2sketch_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 TestSketch(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tcs := []testCase{
|
||||
{
|
||||
name: "basic",
|
||||
script: `a -> b
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "child to child",
|
||||
script: `winter.snow -> summer.sun
|
||||
`,
|
||||
},
|
||||
{
|
||||
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 {
|
||||
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" {
|
||||
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 {
|
||||
# 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 {
|
||||
fill: "#ffdef1"
|
||||
}
|
||||
manhattan: Manhattan
|
||||
memcache: Memcache {
|
||||
icon: https://d1q6f0aelx0por.cloudfront.net/product-logos/de041504-0ddb-43f6-b89e-fe04403cca8d-memcached.png
|
||||
}
|
||||
|
||||
fetch: Fetch {
|
||||
multiple: true
|
||||
shape: step
|
||||
}
|
||||
feature: Feature {
|
||||
multiple: true
|
||||
shape: step
|
||||
}
|
||||
scoring: Scoring {
|
||||
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",
|
||||
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)
|
||||
}
|
||||
`,
|
||||
},
|
||||
}
|
||||
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,
|
||||
ThemeID: 0,
|
||||
Layout: d2dagrelayout.Layout,
|
||||
FontFamily: go2.Pointer(d2fonts.HandDrawn),
|
||||
})
|
||||
if !tassert.Nil(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
dataPath := filepath.Join("testdata", strings.TrimPrefix(t.Name(), "TestSketch/"))
|
||||
pathGotSVG := filepath.Join(dataPath, "sketch.got.svg")
|
||||
|
||||
svgBytes, err := d2svg.Render(diagram, &d2svg.RenderOpts{
|
||||
Pad: d2svg.DEFAULT_PADDING,
|
||||
Sketch: true,
|
||||
})
|
||||
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, "sketch"), ".svg", svgBytes)
|
||||
assert.Success(t, err)
|
||||
}
|
||||
43
d2renderers/d2sketch/testdata/all_shapes/sketch.exp.svg
vendored
Normal file
|
After Width: | Height: | Size: 298 KiB |
43
d2renderers/d2sketch/testdata/basic/sketch.exp.svg
vendored
Normal file
|
After Width: | Height: | Size: 196 KiB |
50
d2renderers/d2sketch/testdata/child_to_child/sketch.exp.svg
vendored
Normal file
|
After Width: | Height: | Size: 250 KiB |
55
d2renderers/d2sketch/testdata/class/sketch.exp.svg
vendored
Normal file
|
After Width: | Height: | Size: 196 KiB |
136
d2renderers/d2sketch/testdata/connection_label/board.exp.json
generated
vendored
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
]
|
||||
}
|
||||
50
d2renderers/d2sketch/testdata/connection_label/sketch.exp.svg
vendored
Normal file
|
After Width: | Height: | Size: 246 KiB |
75
d2renderers/d2sketch/testdata/sql_tables/sketch.exp.svg
vendored
Normal file
|
After Width: | Height: | Size: 86 KiB |
828
d2renderers/d2sketch/testdata/twitter/sketch.exp.svg
vendored
Normal file
|
After Width: | Height: | Size: 384 KiB |
186
d2renderers/d2svg/appendix/appendix.go
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
// appendix.go writes appendices/footnotes to SVG
|
||||
// Intended to be run only for static exports, like PNG or PDF.
|
||||
// SVG exports are already interactive.
|
||||
|
||||
package appendix
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"oss.terrastruct.com/d2/d2graph"
|
||||
"oss.terrastruct.com/d2/d2renderers/d2fonts"
|
||||
"oss.terrastruct.com/d2/d2renderers/d2svg"
|
||||
"oss.terrastruct.com/d2/d2target"
|
||||
"oss.terrastruct.com/d2/lib/textmeasure"
|
||||
"oss.terrastruct.com/util-go/go2"
|
||||
)
|
||||
|
||||
// ┌──────────────┐
|
||||
// │ │
|
||||
// │ DIAGRAM │
|
||||
// │ │
|
||||
// PAD_ │ │
|
||||
// SIDES │ │
|
||||
// │ │ │
|
||||
// │ └──────────────┘
|
||||
// ▼ ◄────── PAD_TOP
|
||||
//
|
||||
// ─────────────────────────
|
||||
//
|
||||
//
|
||||
// 1. asdfasdf
|
||||
//
|
||||
// ◄──── SPACER
|
||||
// 2. qwerqwer
|
||||
//
|
||||
//
|
||||
|
||||
const (
|
||||
PAD_TOP = 50
|
||||
PAD_SIDES = 40
|
||||
SPACER = 20
|
||||
|
||||
FONT_SIZE = 16
|
||||
ICON_RADIUS = 16
|
||||
)
|
||||
|
||||
var viewboxRegex = regexp.MustCompile(`viewBox=\"([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)
|
||||
|
||||
appendix, w, h := generateAppendix(diagram, ruler, svg)
|
||||
|
||||
if h == 0 {
|
||||
return in
|
||||
}
|
||||
|
||||
viewboxMatch := viewboxRegex.FindStringSubmatch(svg)
|
||||
viewboxRaw := viewboxMatch[1]
|
||||
viewboxSlice := strings.Split(viewboxRaw, " ")
|
||||
viewboxPadLeft, _ := strconv.Atoi(viewboxSlice[0])
|
||||
viewboxWidth, _ := strconv.Atoi(viewboxSlice[2])
|
||||
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
|
||||
|
||||
w -= viewboxPadLeft
|
||||
w += PAD_SIDES * 2
|
||||
if viewboxWidth < w {
|
||||
viewboxWidth = w
|
||||
}
|
||||
|
||||
viewboxHeight += h + PAD_TOP
|
||||
|
||||
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)
|
||||
newWidth := fmt.Sprintf(`width="%s"`, strconv.Itoa(viewboxWidth))
|
||||
newHeight := fmt.Sprintf(`height="%s"`, strconv.Itoa(viewboxHeight))
|
||||
|
||||
svg = strings.Replace(svg, viewboxMatch[0], newViewbox, 1)
|
||||
svg = strings.Replace(svg, widthMatch[0], newWidth, 1)
|
||||
svg = strings.Replace(svg, heightMatch[0], newHeight, 1)
|
||||
|
||||
if !strings.Contains(svg, `font-family: "font-regular"`) {
|
||||
appendix += fmt.Sprintf(`<style type="text/css"><![CDATA[
|
||||
.text {
|
||||
font-family: "font-regular";
|
||||
}
|
||||
@font-face {
|
||||
font-family: font-regular;
|
||||
src: url("%s");
|
||||
}
|
||||
]]></style>`, d2fonts.FontEncodings[d2fonts.SourceSansPro.Font(0, d2fonts.FONT_STYLE_REGULAR)])
|
||||
}
|
||||
if !strings.Contains(svg, `font-family: "font-bold"`) {
|
||||
appendix += fmt.Sprintf(`<style type="text/css"><![CDATA[
|
||||
.text-bold {
|
||||
font-family: "font-bold";
|
||||
}
|
||||
@font-face {
|
||||
font-family: font-bold;
|
||||
src: url("%s");
|
||||
}
|
||||
]]></style>`, d2fonts.FontEncodings[d2fonts.SourceSansPro.Font(0, d2fonts.FONT_STYLE_BOLD)])
|
||||
}
|
||||
|
||||
closingIndex := strings.LastIndex(svg, "</svg>")
|
||||
svg = svg[:closingIndex] + appendix + svg[closingIndex:]
|
||||
|
||||
i := 1
|
||||
for _, s := range diagram.Shapes {
|
||||
if s.Tooltip != "" {
|
||||
// The clip-path has a unique ID, so this won't replace any user icons
|
||||
// In the existing SVG, the transform places it top-left, so we adjust
|
||||
svg = strings.Replace(svg, d2svg.TooltipIcon, generateNumberedIcon(i, 0, ICON_RADIUS), 1)
|
||||
i++
|
||||
}
|
||||
if s.Link != "" {
|
||||
svg = strings.Replace(svg, d2svg.LinkIcon, generateNumberedIcon(i, 0, ICON_RADIUS), 1)
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
return []byte(svg)
|
||||
}
|
||||
|
||||
func generateAppendix(diagram *d2target.Diagram, ruler *textmeasure.Ruler, svg string) (string, int, int) {
|
||||
tl, br := diagram.BoundingBox()
|
||||
|
||||
maxWidth, totalHeight := 0, 0
|
||||
|
||||
var lines []string
|
||||
i := 1
|
||||
|
||||
for _, s := range diagram.Shapes {
|
||||
for _, txt := range []string{s.Tooltip, s.Link} {
|
||||
if txt != "" {
|
||||
line, w, h := generateLine(i, br.Y+(PAD_TOP*2)+totalHeight, txt, ruler)
|
||||
i++
|
||||
lines = append(lines, line)
|
||||
maxWidth = go2.IntMax(maxWidth, w)
|
||||
totalHeight += h + SPACER
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Sprintf(`<g x="%d" y="%d" width="%d" height="100%%">%s</g>
|
||||
`, tl.X, br.Y, (br.X - tl.X), strings.Join(lines, "\n")), maxWidth, totalHeight
|
||||
}
|
||||
|
||||
func generateNumberedIcon(i, x, y int) string {
|
||||
line := fmt.Sprintf(`<circle cx="%d" cy="%d" r="%d" fill="white" stroke="#DEE1EB" />`,
|
||||
x+ICON_RADIUS, y, ICON_RADIUS)
|
||||
|
||||
line += fmt.Sprintf(`<text class="text-bold" x="%d" y="%d" style="font-size: %dpx;text-anchor:middle;">%d</text>`,
|
||||
x+ICON_RADIUS, y+5, FONT_SIZE, i)
|
||||
|
||||
return line
|
||||
}
|
||||
|
||||
func generateLine(i, y int, text string, ruler *textmeasure.Ruler) (string, int, int) {
|
||||
mtext := &d2target.MText{
|
||||
Text: text,
|
||||
FontSize: FONT_SIZE,
|
||||
}
|
||||
|
||||
dims := d2graph.GetTextDimensions(nil, ruler, mtext, nil)
|
||||
|
||||
line := fmt.Sprintf(`<g transform="translate(%d %d)" class="appendix-icon">%s</g>`,
|
||||
0, y, generateNumberedIcon(i, 0, 0))
|
||||
|
||||
line += fmt.Sprintf(`<text class="text" x="%d" y="%d" style="font-size: %dpx;">%s</text>`,
|
||||
ICON_RADIUS*3, y, FONT_SIZE, d2svg.RenderText(text, ICON_RADIUS*3, float64(dims.Height)))
|
||||
|
||||
return line, dims.Width + ICON_RADIUS*3, dims.Height
|
||||
}
|
||||
151
d2renderers/d2svg/appendix/appendix_test.go
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
package appendix_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/d2/d2layouts/d2dagrelayout"
|
||||
"oss.terrastruct.com/d2/d2lib"
|
||||
"oss.terrastruct.com/d2/d2renderers/d2svg"
|
||||
"oss.terrastruct.com/d2/d2renderers/d2svg/appendix"
|
||||
"oss.terrastruct.com/d2/lib/log"
|
||||
"oss.terrastruct.com/d2/lib/textmeasure"
|
||||
)
|
||||
|
||||
func TestAppendix(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tcs := []testCase{
|
||||
{
|
||||
name: "tooltip_wider_than_diagram",
|
||||
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
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "diagram_wider_than_tooltip",
|
||||
script: `shape: sequence_diagram
|
||||
|
||||
customer
|
||||
issuer
|
||||
store: { tooltip: Like starbucks or something }
|
||||
acquirer: { tooltip: I'm not sure what this is }
|
||||
network
|
||||
customer bank
|
||||
store bank
|
||||
|
||||
customer: {shape: person}
|
||||
customer bank: {
|
||||
shape: image
|
||||
icon: https://cdn-icons-png.flaticon.com/512/858/858170.png
|
||||
}
|
||||
store bank: {
|
||||
shape: image
|
||||
icon: https://cdn-icons-png.flaticon.com/512/858/858170.png
|
||||
}
|
||||
|
||||
initial transaction: {
|
||||
customer -> store: 1 banana please
|
||||
store -> customer: '$10 dollars'
|
||||
}
|
||||
customer.internal -> customer.internal: "thinking: wow, inflation"
|
||||
customer.internal -> customer bank: checks bank account
|
||||
customer bank -> customer.internal: 'Savings: $11'
|
||||
customer."An error in judgement is about to occur"
|
||||
customer -> store: I can do that, here's my card
|
||||
payment processor behind the scenes: {
|
||||
store -> acquirer: Run this card
|
||||
acquirer -> network: Process to card issuer
|
||||
simplified: {
|
||||
network -> issuer: Process this payment
|
||||
issuer -> customer bank: '$10 debit'
|
||||
acquirer -> store bank: '$10 credit'
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "links",
|
||||
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
|
||||
`,
|
||||
},
|
||||
}
|
||||
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,
|
||||
ThemeID: 0,
|
||||
Layout: d2dagrelayout.Layout,
|
||||
})
|
||||
if !tassert.Nil(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
dataPath := filepath.Join("testdata", strings.TrimPrefix(t.Name(), "TestAppendix/"))
|
||||
pathGotSVG := filepath.Join(dataPath, "sketch.got.svg")
|
||||
|
||||
svgBytes, err := d2svg.Render(diagram, &d2svg.RenderOpts{
|
||||
Pad: d2svg.DEFAULT_PADDING,
|
||||
})
|
||||
assert.Success(t, err)
|
||||
svgBytes = appendix.Append(diagram, ruler, svgBytes)
|
||||
|
||||
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, "sketch"), ".svg", svgBytes)
|
||||
assert.Success(t, err)
|
||||
}
|
||||
64
d2renderers/d2svg/appendix/testdata/diagram_wider_than_tooltip/sketch.exp.svg
vendored
Normal file
|
After Width: | Height: | Size: 803 KiB |
46
d2renderers/d2svg/appendix/testdata/links/sketch.exp.svg
vendored
Normal file
|
After Width: | Height: | Size: 650 KiB |
45
d2renderers/d2svg/appendix/testdata/tooltip_wider_than_diagram/sketch.exp.svg
vendored
Normal file
|
After Width: | Height: | Size: 650 KiB |
|
|
@ -8,11 +8,12 @@ import (
|
|||
"oss.terrastruct.com/d2/d2target"
|
||||
"oss.terrastruct.com/d2/lib/geo"
|
||||
"oss.terrastruct.com/d2/lib/label"
|
||||
"oss.terrastruct.com/d2/lib/svg"
|
||||
)
|
||||
|
||||
func classHeader(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="black" />`,
|
||||
box.TopLeft.X, box.TopLeft.Y, box.Width, box.Height)
|
||||
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)
|
||||
|
||||
if text != "" {
|
||||
tl := label.InsideMiddleCenter.GetPointOnBox(
|
||||
|
|
@ -23,79 +24,60 @@ func classHeader(box *geo.Box, text string, textWidth, textHeight, fontSize floa
|
|||
)
|
||||
|
||||
str += fmt.Sprintf(`<text class="%s" x="%f" y="%f" style="%s">%s</text>`,
|
||||
// TODO use monospace font
|
||||
"text",
|
||||
"text-mono",
|
||||
tl.X+textWidth/2,
|
||||
tl.Y+textHeight*3/4,
|
||||
fmt.Sprintf("text-anchor:%s;font-size:%vpx;fill:%s",
|
||||
fmt.Sprintf(`text-anchor:%s;font-size:%vpx;fill:%s`,
|
||||
"middle",
|
||||
4+fontSize,
|
||||
"white",
|
||||
shape.Stroke,
|
||||
),
|
||||
escapeText(text),
|
||||
svg.EscapeText(text),
|
||||
)
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
const (
|
||||
prefixPadding = 10
|
||||
prefixWidth = 20
|
||||
typePadding = 20
|
||||
)
|
||||
|
||||
func classRow(box *geo.Box, prefix, nameText, typeText string, fontSize float64) string {
|
||||
func classRow(shape d2target.Shape, box *geo.Box, prefix, nameText, typeText string, fontSize float64) string {
|
||||
// Row is made up of prefix, name, and type
|
||||
// e.g. | + firstName string |
|
||||
prefixTL := label.InsideMiddleLeft.GetPointOnBox(
|
||||
box,
|
||||
prefixPadding,
|
||||
d2target.PrefixPadding,
|
||||
box.Width,
|
||||
fontSize,
|
||||
)
|
||||
typeTR := label.InsideMiddleRight.GetPointOnBox(
|
||||
box,
|
||||
typePadding,
|
||||
d2target.TypePadding,
|
||||
0,
|
||||
fontSize,
|
||||
)
|
||||
accentColor := "rgb(13, 50, 178)"
|
||||
|
||||
return strings.Join([]string{
|
||||
fmt.Sprintf(`<text class="text" x="%f" y="%f" style="%s">%s</text>`,
|
||||
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, accentColor),
|
||||
fmt.Sprintf("text-anchor:%s;font-size:%vpx;fill:%s", "start", fontSize, shape.PrimaryAccentColor),
|
||||
prefix,
|
||||
),
|
||||
|
||||
fmt.Sprintf(`<text class="text" x="%f" y="%f" style="%s">%s</text>`,
|
||||
prefixTL.X+prefixWidth,
|
||||
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, "black"),
|
||||
escapeText(nameText),
|
||||
fmt.Sprintf("text-anchor:%s;font-size:%vpx;fill:%s", "start", fontSize, shape.Fill),
|
||||
svg.EscapeText(nameText),
|
||||
),
|
||||
|
||||
fmt.Sprintf(`<text class="text" x="%f" y="%f" style="%s">%s</text>`,
|
||||
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, accentColor),
|
||||
escapeText(typeText),
|
||||
fmt.Sprintf("text-anchor:%s;font-size:%vpx;fill:%s", "end", fontSize, shape.SecondaryAccentColor),
|
||||
svg.EscapeText(typeText),
|
||||
),
|
||||
}, "\n")
|
||||
}
|
||||
|
||||
func visibilityToken(visibility string) string {
|
||||
switch visibility {
|
||||
case "protected":
|
||||
return "#"
|
||||
case "private":
|
||||
return "-"
|
||||
default:
|
||||
return "+"
|
||||
}
|
||||
}
|
||||
|
||||
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, shapeStyle(targetShape))
|
||||
|
|
@ -109,14 +91,14 @@ func drawClass(writer io.Writer, targetShape d2target.Shape) {
|
|||
headerBox := geo.NewBox(box.TopLeft, box.Width, 2*rowHeight)
|
||||
|
||||
fmt.Fprint(writer,
|
||||
classHeader(headerBox, targetShape.Label, float64(targetShape.LabelWidth), float64(targetShape.LabelHeight), float64(targetShape.FontSize)),
|
||||
classHeader(targetShape, headerBox, targetShape.Label, float64(targetShape.LabelWidth), float64(targetShape.LabelHeight), float64(targetShape.FontSize)),
|
||||
)
|
||||
|
||||
rowBox := geo.NewBox(box.TopLeft.Copy(), box.Width, rowHeight)
|
||||
rowBox.TopLeft.Y += headerBox.Height
|
||||
for _, f := range targetShape.Class.Fields {
|
||||
for _, f := range targetShape.Fields {
|
||||
fmt.Fprint(writer,
|
||||
classRow(rowBox, visibilityToken(f.Visibility), f.Name, f.Type, float64(targetShape.FontSize)),
|
||||
classRow(targetShape, rowBox, f.VisibilityToken(), f.Name, f.Type, float64(targetShape.FontSize)),
|
||||
)
|
||||
rowBox.TopLeft.Y += rowHeight
|
||||
}
|
||||
|
|
@ -124,11 +106,11 @@ func drawClass(writer io.Writer, targetShape d2target.Shape) {
|
|||
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.Stroke))
|
||||
fmt.Sprintf("stroke-width:1;stroke:%v", targetShape.Fill))
|
||||
|
||||
for _, m := range targetShape.Class.Methods {
|
||||
for _, m := range targetShape.Methods {
|
||||
fmt.Fprint(writer,
|
||||
classRow(rowBox, visibilityToken(m.Visibility), m.Name, m.Return, float64(targetShape.FontSize)),
|
||||
classRow(targetShape, rowBox, m.VisibilityToken(), m.Name, m.Return, float64(targetShape.FontSize)),
|
||||
)
|
||||
rowBox.TopLeft.Y += rowHeight
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,10 +5,10 @@ package d2svg
|
|||
import (
|
||||
"bytes"
|
||||
_ "embed"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"html"
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
|
|
@ -25,32 +25,50 @@ import (
|
|||
"oss.terrastruct.com/d2/d2graph"
|
||||
"oss.terrastruct.com/d2/d2renderers/d2fonts"
|
||||
"oss.terrastruct.com/d2/d2renderers/d2latex"
|
||||
"oss.terrastruct.com/d2/d2renderers/d2sketch"
|
||||
"oss.terrastruct.com/d2/d2target"
|
||||
"oss.terrastruct.com/d2/lib/color"
|
||||
"oss.terrastruct.com/d2/lib/geo"
|
||||
"oss.terrastruct.com/d2/lib/label"
|
||||
"oss.terrastruct.com/d2/lib/shape"
|
||||
"oss.terrastruct.com/d2/lib/svg"
|
||||
"oss.terrastruct.com/d2/lib/textmeasure"
|
||||
)
|
||||
|
||||
const (
|
||||
padding = 100
|
||||
DEFAULT_PADDING = 100
|
||||
MIN_ARROWHEAD_STROKE_WIDTH = 2
|
||||
threeDeeOffset = 15
|
||||
|
||||
appendixIconRadius = 16
|
||||
)
|
||||
|
||||
var multipleOffset = geo.NewVector(10, -10)
|
||||
|
||||
//go:embed tooltip.svg
|
||||
var TooltipIcon string
|
||||
|
||||
//go:embed link.svg
|
||||
var LinkIcon string
|
||||
|
||||
//go:embed style.css
|
||||
var styleCSS string
|
||||
|
||||
//go:embed sketchstyle.css
|
||||
var sketchStyleCSS string
|
||||
|
||||
//go:embed github-markdown.css
|
||||
var mdCSS string
|
||||
|
||||
func setViewbox(writer io.Writer, diagram *d2target.Diagram) (width int, height int) {
|
||||
type RenderOpts struct {
|
||||
Pad int
|
||||
Sketch bool
|
||||
}
|
||||
|
||||
func setViewbox(writer io.Writer, diagram *d2target.Diagram, pad int) (width int, height int) {
|
||||
tl, br := diagram.BoundingBox()
|
||||
w := br.X - tl.X + padding*2
|
||||
h := br.Y - tl.Y + padding*2
|
||||
w := br.X - tl.X + pad*2
|
||||
h := br.Y - tl.Y + pad*2
|
||||
// TODO minify
|
||||
|
||||
// TODO background stuff. e.g. dotted, grid, colors
|
||||
|
|
@ -58,7 +76,7 @@ func setViewbox(writer io.Writer, diagram *d2target.Diagram) (width int, height
|
|||
<svg
|
||||
style="background: white;"
|
||||
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
width="%d" height="%d" viewBox="%d %d %d %d">`, w, h, tl.X-padding, tl.Y-padding, w, h)
|
||||
width="%d" height="%d" viewBox="%d %d %d %d">`, w, h, tl.X-pad, tl.Y-pad, w, h)
|
||||
|
||||
return w, h
|
||||
}
|
||||
|
|
@ -346,8 +364,8 @@ func makeLabelMask(labelTL *geo.Point, width, height int) string {
|
|||
)
|
||||
}
|
||||
|
||||
func drawConnection(writer io.Writer, labelMaskID string, connection d2target.Connection, markers map[string]struct{}, idToShape map[string]d2target.Shape) (labelMask string) {
|
||||
fmt.Fprintf(writer, `<g id="%s">`, escapeText(connection.ID))
|
||||
func drawConnection(writer io.Writer, labelMaskID string, connection d2target.Connection, markers map[string]struct{}, idToShape map[string]d2target.Shape, sketchRunner *d2sketch.Runner) (labelMask string, _ error) {
|
||||
fmt.Fprintf(writer, `<g id="%s">`, svg.EscapeText(connection.ID))
|
||||
var markerStart string
|
||||
if connection.SrcArrow != d2target.NoArrowhead {
|
||||
id := arrowheadMarkerID(false, connection)
|
||||
|
|
@ -383,43 +401,26 @@ func drawConnection(writer io.Writer, labelMaskID string, connection d2target.Co
|
|||
labelTL.Y = math.Round(labelTL.Y)
|
||||
|
||||
if label.Position(connection.LabelPosition).IsOnEdge() {
|
||||
strokeWidth := float64(connection.StrokeWidth)
|
||||
tl, br := geo.Route(connection.Route).GetBoundingBox()
|
||||
tl.X -= strokeWidth
|
||||
tl.Y -= strokeWidth
|
||||
br.X += strokeWidth
|
||||
br.Y += strokeWidth
|
||||
if connection.SrcArrow != d2target.NoArrowhead {
|
||||
width, height := arrowheadDimensions(connection.SrcArrow, strokeWidth)
|
||||
tl.X -= width
|
||||
tl.Y -= height
|
||||
br.X += width
|
||||
br.Y += height
|
||||
}
|
||||
if connection.DstArrow != d2target.NoArrowhead {
|
||||
width, height := arrowheadDimensions(connection.DstArrow, strokeWidth)
|
||||
tl.X -= width
|
||||
tl.Y -= height
|
||||
br.X += width
|
||||
br.Y += height
|
||||
}
|
||||
|
||||
tl.X = math.Min(tl.X, labelTL.X)
|
||||
tl.Y = math.Min(tl.Y, labelTL.Y)
|
||||
br.X = math.Max(br.X, labelTL.X+float64(connection.LabelWidth))
|
||||
br.Y = math.Max(br.Y, labelTL.Y+float64(connection.LabelHeight))
|
||||
|
||||
labelMask = makeLabelMask(labelTL, connection.LabelWidth, connection.LabelHeight)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Fprintf(writer, `<path d="%s" class="connection" style="fill:none;%s" %s%smask="url(#%s)"/>`,
|
||||
pathData(connection, idToShape),
|
||||
connectionStyle(connection),
|
||||
path := pathData(connection, idToShape)
|
||||
attrs := fmt.Sprintf(`%s%smask="url(#%s)"`,
|
||||
markerStart,
|
||||
markerEnd,
|
||||
labelMaskID,
|
||||
)
|
||||
if sketchRunner != nil {
|
||||
out, err := d2sketch.Connection(sketchRunner, connection, path, attrs)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
fmt.Fprintf(writer, out)
|
||||
} else {
|
||||
fmt.Fprintf(writer, `<path d="%s" class="connection" style="fill:none;%s" %s/>`,
|
||||
path, connectionStyle(connection), attrs)
|
||||
}
|
||||
|
||||
if connection.Label != "" {
|
||||
fontClass := "text"
|
||||
|
|
@ -432,6 +433,11 @@ func drawConnection(writer io.Writer, labelMaskID string, connection d2target.Co
|
|||
if connection.Color != "" {
|
||||
fontColor = connection.Color
|
||||
}
|
||||
|
||||
if connection.Fill != "" {
|
||||
fmt.Fprintf(writer, `<rect x="%f" y="%f" width="%d" height="%d" style="fill:%s" />`,
|
||||
labelTL.X, labelTL.Y, connection.LabelWidth, connection.LabelHeight, connection.Fill)
|
||||
}
|
||||
textStyle := fmt.Sprintf("text-anchor:%s;font-size:%vpx;fill:%s", "middle", connection.FontSize, fontColor)
|
||||
x := labelTL.X + float64(connection.LabelWidth)/2
|
||||
y := labelTL.Y + float64(connection.FontSize)
|
||||
|
|
@ -439,7 +445,7 @@ func drawConnection(writer io.Writer, labelMaskID string, connection d2target.Co
|
|||
fontClass,
|
||||
x, y,
|
||||
textStyle,
|
||||
renderText(connection.Label, x, float64(connection.LabelHeight)),
|
||||
RenderText(connection.Label, x, float64(connection.LabelHeight)),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -475,7 +481,7 @@ func renderArrowheadLabel(connection d2target.Connection, text string, position,
|
|||
return fmt.Sprintf(`<text class="text-italic" x="%f" y="%f" style="%s">%s</text>`,
|
||||
x, y,
|
||||
textStyle,
|
||||
renderText(text, x, height),
|
||||
RenderText(text, x, height),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -538,7 +544,7 @@ func render3dRect(targetShape d2target.Shape) string {
|
|||
strings.Join(borderSegments, " "), borderStyle)
|
||||
|
||||
// create mask from border stroke, to cut away from the shape fills
|
||||
maskID := fmt.Sprintf("border-mask-%v", escapeText(targetShape.ID))
|
||||
maskID := fmt.Sprintf("border-mask-%v", svg.EscapeText(targetShape.ID))
|
||||
borderMask := strings.Join([]string{
|
||||
fmt.Sprintf(`<defs><mask id="%s" maskUnits="userSpaceOnUse" x="%d" y="%d" width="%d" height="%d">`,
|
||||
maskID, targetShape.Pos.X, targetShape.Pos.Y-threeDeeOffset, targetShape.Width+threeDeeOffset, targetShape.Height+threeDeeOffset,
|
||||
|
|
@ -584,8 +590,13 @@ func render3dRect(targetShape d2target.Shape) string {
|
|||
return borderMask + mainRect + renderedSides + renderedBorder
|
||||
}
|
||||
|
||||
func drawShape(writer io.Writer, targetShape d2target.Shape) (labelMask string, err error) {
|
||||
fmt.Fprintf(writer, `<g id="%s">`, escapeText(targetShape.ID))
|
||||
func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2sketch.Runner) (labelMask string, err error) {
|
||||
closingTag := "</g>"
|
||||
if targetShape.Link != "" {
|
||||
fmt.Fprintf(writer, `<a href="%s" xlink:href="%[1]s">`, targetShape.Link)
|
||||
closingTag += "</a>"
|
||||
}
|
||||
fmt.Fprintf(writer, `<g id="%s">`, svg.EscapeText(targetShape.ID))
|
||||
tl := geo.NewPoint(float64(targetShape.Pos.X), float64(targetShape.Pos.Y))
|
||||
width := float64(targetShape.Width)
|
||||
height := float64(targetShape.Height)
|
||||
|
|
@ -620,22 +631,48 @@ func drawShape(writer io.Writer, targetShape d2target.Shape) (labelMask string,
|
|||
|
||||
switch targetShape.Type {
|
||||
case d2target.ShapeClass:
|
||||
drawClass(writer, targetShape)
|
||||
fmt.Fprintf(writer, `</g></g>`)
|
||||
if sketchRunner != nil {
|
||||
out, err := d2sketch.Class(sketchRunner, targetShape)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
fmt.Fprintf(writer, out)
|
||||
} else {
|
||||
drawClass(writer, targetShape)
|
||||
}
|
||||
fmt.Fprintf(writer, `</g>`)
|
||||
fmt.Fprintf(writer, closingTag)
|
||||
return labelMask, nil
|
||||
case d2target.ShapeSQLTable:
|
||||
drawTable(writer, targetShape)
|
||||
fmt.Fprintf(writer, `</g></g>`)
|
||||
if sketchRunner != nil {
|
||||
out, err := d2sketch.Table(sketchRunner, targetShape)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
fmt.Fprintf(writer, out)
|
||||
} else {
|
||||
drawTable(writer, targetShape)
|
||||
}
|
||||
fmt.Fprintf(writer, `</g>`)
|
||||
fmt.Fprintf(writer, closingTag)
|
||||
return labelMask, nil
|
||||
case d2target.ShapeOval:
|
||||
if targetShape.Multiple {
|
||||
fmt.Fprint(writer, renderOval(multipleTL, width, height, style))
|
||||
}
|
||||
fmt.Fprint(writer, renderOval(tl, width, height, style))
|
||||
if sketchRunner != nil {
|
||||
out, err := d2sketch.Oval(sketchRunner, targetShape)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
fmt.Fprintf(writer, out)
|
||||
} else {
|
||||
fmt.Fprint(writer, renderOval(tl, width, height, style))
|
||||
}
|
||||
|
||||
case d2target.ShapeImage:
|
||||
fmt.Fprintf(writer, `<image href="%s" x="%d" y="%d" width="%d" height="%d" style="%s" />`,
|
||||
targetShape.Icon.String(),
|
||||
html.EscapeString(targetShape.Icon.String()),
|
||||
targetShape.Pos.X, targetShape.Pos.Y, targetShape.Width, targetShape.Height, style)
|
||||
|
||||
// TODO should standardize "" to rectangle
|
||||
|
|
@ -647,8 +684,16 @@ func drawShape(writer io.Writer, targetShape d2target.Shape) (labelMask string,
|
|||
fmt.Fprintf(writer, `<rect x="%d" y="%d" width="%d" height="%d" style="%s" />`,
|
||||
targetShape.Pos.X+10, targetShape.Pos.Y-10, targetShape.Width, targetShape.Height, style)
|
||||
}
|
||||
fmt.Fprintf(writer, `<rect x="%d" y="%d" width="%d" height="%d" style="%s" />`,
|
||||
targetShape.Pos.X, targetShape.Pos.Y, targetShape.Width, targetShape.Height, style)
|
||||
if sketchRunner != nil {
|
||||
out, err := d2sketch.Rect(sketchRunner, targetShape)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
fmt.Fprintf(writer, out)
|
||||
} else {
|
||||
fmt.Fprintf(writer, `<rect x="%d" y="%d" width="%d" height="%d" style="%s" />`,
|
||||
targetShape.Pos.X, targetShape.Pos.Y, targetShape.Width, targetShape.Height, style)
|
||||
}
|
||||
}
|
||||
case d2target.ShapeText, d2target.ShapeCode:
|
||||
default:
|
||||
|
|
@ -659,11 +704,20 @@ func drawShape(writer io.Writer, targetShape d2target.Shape) (labelMask string,
|
|||
}
|
||||
}
|
||||
|
||||
for _, pathData := range s.GetSVGPathData() {
|
||||
fmt.Fprintf(writer, `<path d="%s" style="%s"/>`, pathData, style)
|
||||
if sketchRunner != nil {
|
||||
out, err := d2sketch.Paths(sketchRunner, targetShape, s.GetSVGPathData())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
fmt.Fprintf(writer, out)
|
||||
} else {
|
||||
for _, pathData := range s.GetSVGPathData() {
|
||||
fmt.Fprintf(writer, `<path d="%s" style="%s"/>`, pathData, style)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Closes the class=shape
|
||||
fmt.Fprintf(writer, `</g>`)
|
||||
|
||||
if targetShape.Icon != nil && targetShape.Type != d2target.ShapeImage {
|
||||
|
|
@ -679,7 +733,7 @@ func drawShape(writer io.Writer, targetShape d2target.Shape) (labelMask string,
|
|||
tl := iconPosition.GetPointOnBox(box, label.PADDING, float64(iconSize), float64(iconSize))
|
||||
|
||||
fmt.Fprintf(writer, `<image href="%s" x="%f" y="%f" width="%d" height="%d" />`,
|
||||
targetShape.Icon.String(),
|
||||
html.EscapeString(targetShape.Icon.String()),
|
||||
tl.X,
|
||||
tl.Y,
|
||||
iconSize,
|
||||
|
|
@ -703,9 +757,11 @@ func drawShape(writer io.Writer, targetShape d2target.Shape) (labelMask string,
|
|||
} else if targetShape.Italic {
|
||||
fontClass += "-italic"
|
||||
}
|
||||
if targetShape.Underline {
|
||||
fontClass += " text-underline"
|
||||
}
|
||||
|
||||
switch targetShape.Type {
|
||||
case d2target.ShapeCode:
|
||||
if targetShape.Type == d2target.ShapeCode {
|
||||
lexer := lexers.Get(targetShape.Language)
|
||||
if lexer == nil {
|
||||
return labelMask, fmt.Errorf("code snippet lexer for %s not found", targetShape.Language)
|
||||
|
|
@ -746,29 +802,36 @@ func drawShape(writer io.Writer, targetShape d2target.Shape) (labelMask string,
|
|||
fmt.Fprint(writer, "</text>")
|
||||
}
|
||||
fmt.Fprintf(writer, "</g></g>")
|
||||
case d2target.ShapeText:
|
||||
if targetShape.Language == "latex" {
|
||||
render, err := d2latex.Render(targetShape.Label)
|
||||
if err != nil {
|
||||
return labelMask, err
|
||||
}
|
||||
fmt.Fprintf(writer, `<g transform="translate(%f %f)" style="opacity:%f">`, box.TopLeft.X, box.TopLeft.Y, targetShape.Opacity)
|
||||
fmt.Fprint(writer, render)
|
||||
fmt.Fprintf(writer, "</g>")
|
||||
} else {
|
||||
render, err := textmeasure.RenderMarkdown(targetShape.Label)
|
||||
if err != nil {
|
||||
return labelMask, err
|
||||
}
|
||||
fmt.Fprintf(writer, `<g><foreignObject requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" x="%f" y="%f" width="%d" height="%d">`,
|
||||
box.TopLeft.X, box.TopLeft.Y, targetShape.Width, targetShape.Height,
|
||||
)
|
||||
// we need the self closing form in this svg/xhtml context
|
||||
render = strings.ReplaceAll(render, "<hr>", "<hr />")
|
||||
fmt.Fprintf(writer, `<div xmlns="http://www.w3.org/1999/xhtml" class="md">%v</div>`, render)
|
||||
fmt.Fprint(writer, `</foreignObject></g>`)
|
||||
} else if targetShape.Type == d2target.ShapeText && targetShape.Language == "latex" {
|
||||
render, err := d2latex.Render(targetShape.Label)
|
||||
if err != nil {
|
||||
return labelMask, err
|
||||
}
|
||||
default:
|
||||
fmt.Fprintf(writer, `<g transform="translate(%f %f)" style="opacity:%f">`, box.TopLeft.X, box.TopLeft.Y, targetShape.Opacity)
|
||||
fmt.Fprint(writer, render)
|
||||
fmt.Fprintf(writer, "</g>")
|
||||
} else if targetShape.Type == d2target.ShapeText && targetShape.Language != "" {
|
||||
render, err := textmeasure.RenderMarkdown(targetShape.Label)
|
||||
if err != nil {
|
||||
return labelMask, err
|
||||
}
|
||||
fmt.Fprintf(writer, `<g><foreignObject requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" x="%f" y="%f" width="%d" height="%d">`,
|
||||
box.TopLeft.X, box.TopLeft.Y, targetShape.Width, targetShape.Height,
|
||||
)
|
||||
// we need the self closing form in this svg/xhtml context
|
||||
render = strings.ReplaceAll(render, "<hr>", "<hr />")
|
||||
|
||||
var mdStyle string
|
||||
if targetShape.Fill != "" {
|
||||
mdStyle = fmt.Sprintf("background-color:%s;", targetShape.Fill)
|
||||
}
|
||||
if targetShape.Stroke != "" {
|
||||
mdStyle += fmt.Sprintf("color:%s;", targetShape.Stroke)
|
||||
}
|
||||
|
||||
fmt.Fprintf(writer, `<div xmlns="http://www.w3.org/1999/xhtml" class="md" style="%s">%v</div>`, mdStyle, render)
|
||||
fmt.Fprint(writer, `</foreignObject></g>`)
|
||||
} else {
|
||||
fontColor := "black"
|
||||
if targetShape.Color != "" {
|
||||
fontColor = targetShape.Color
|
||||
|
|
@ -781,26 +844,40 @@ func drawShape(writer io.Writer, targetShape d2target.Shape) (labelMask string,
|
|||
fontClass,
|
||||
x, y,
|
||||
textStyle,
|
||||
renderText(targetShape.Label, x, float64(targetShape.LabelHeight)),
|
||||
RenderText(targetShape.Label, x, float64(targetShape.LabelHeight)),
|
||||
)
|
||||
if targetShape.Blend {
|
||||
labelMask = makeLabelMask(labelTL, targetShape.LabelWidth, targetShape.LabelHeight-d2graph.INNER_LABEL_PADDING)
|
||||
}
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(writer, `</g>`)
|
||||
|
||||
rightPadForTooltip := 0
|
||||
if targetShape.Tooltip != "" {
|
||||
rightPadForTooltip = 2 * appendixIconRadius
|
||||
fmt.Fprintf(writer, `<g transform="translate(%d %d)" class="appendix-icon">%s</g>`,
|
||||
targetShape.Pos.X+targetShape.Width-appendixIconRadius,
|
||||
targetShape.Pos.Y-appendixIconRadius,
|
||||
TooltipIcon,
|
||||
)
|
||||
fmt.Fprintf(writer, `<title>%s</title>`, targetShape.Tooltip)
|
||||
}
|
||||
|
||||
if targetShape.Link != "" {
|
||||
fmt.Fprintf(writer, `<g transform="translate(%d %d)" class="appendix-icon">%s</g>`,
|
||||
targetShape.Pos.X+targetShape.Width-appendixIconRadius-rightPadForTooltip,
|
||||
targetShape.Pos.Y-appendixIconRadius,
|
||||
LinkIcon,
|
||||
)
|
||||
}
|
||||
|
||||
fmt.Fprintf(writer, closingTag)
|
||||
return labelMask, nil
|
||||
}
|
||||
|
||||
func escapeText(text string) string {
|
||||
buf := new(bytes.Buffer)
|
||||
_ = xml.EscapeText(buf, []byte(text))
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func renderText(text string, x, height float64) string {
|
||||
func RenderText(text string, x, height float64) string {
|
||||
if !strings.Contains(text, "\n") {
|
||||
return escapeText(text)
|
||||
return svg.EscapeText(text)
|
||||
}
|
||||
rendered := []string{}
|
||||
lines := strings.Split(text, "\n")
|
||||
|
|
@ -809,7 +886,7 @@ func renderText(text string, x, height float64) string {
|
|||
if i == 0 {
|
||||
dy = 0
|
||||
}
|
||||
escaped := escapeText(line)
|
||||
escaped := svg.EscapeText(line)
|
||||
if escaped == "" {
|
||||
// if there are multiple newlines in a row we still need text for the tspan to render
|
||||
escaped = " "
|
||||
|
|
@ -822,12 +899,20 @@ func renderText(text string, x, height float64) string {
|
|||
func shapeStyle(shape d2target.Shape) string {
|
||||
out := ""
|
||||
|
||||
out += fmt.Sprintf(`fill:%s;`, shape.Fill)
|
||||
out += fmt.Sprintf(`stroke:%s;`, shape.Stroke)
|
||||
if shape.Type == d2target.ShapeSQLTable || shape.Type == d2target.ShapeClass {
|
||||
// Fill is used for header fill in these types
|
||||
// This fill property is just background of rows
|
||||
out += fmt.Sprintf(`fill:%s;`, shape.Stroke)
|
||||
// Stroke (border) of these shapes should match the header fill
|
||||
out += fmt.Sprintf(`stroke:%s;`, shape.Fill)
|
||||
} else {
|
||||
out += fmt.Sprintf(`fill:%s;`, shape.Fill)
|
||||
out += fmt.Sprintf(`stroke:%s;`, shape.Stroke)
|
||||
}
|
||||
out += fmt.Sprintf(`opacity:%f;`, shape.Opacity)
|
||||
out += fmt.Sprintf(`stroke-width:%d;`, shape.StrokeWidth)
|
||||
if shape.StrokeDash != 0 {
|
||||
dashSize, gapSize := getStrokeDashAttributes(float64(shape.StrokeWidth), shape.StrokeDash)
|
||||
dashSize, gapSize := svg.GetStrokeDashAttributes(float64(shape.StrokeWidth), shape.StrokeDash)
|
||||
out += fmt.Sprintf(`stroke-dasharray:%f,%f;`, dashSize, gapSize)
|
||||
}
|
||||
|
||||
|
|
@ -841,27 +926,20 @@ func connectionStyle(connection d2target.Connection) string {
|
|||
out += fmt.Sprintf(`opacity:%f;`, connection.Opacity)
|
||||
out += fmt.Sprintf(`stroke-width:%d;`, connection.StrokeWidth)
|
||||
if connection.StrokeDash != 0 {
|
||||
dashSize, gapSize := getStrokeDashAttributes(float64(connection.StrokeWidth), connection.StrokeDash)
|
||||
dashSize, gapSize := svg.GetStrokeDashAttributes(float64(connection.StrokeWidth), connection.StrokeDash)
|
||||
out += fmt.Sprintf(`stroke-dasharray:%f,%f;`, dashSize, gapSize)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func getStrokeDashAttributes(strokeWidth, dashGapSize float64) (float64, float64) {
|
||||
// as the stroke width gets thicker, the dash gap gets smaller
|
||||
scale := math.Log10(-0.6*strokeWidth+10.6)*0.5 + 0.5
|
||||
scaledDashSize := strokeWidth * dashGapSize
|
||||
scaledGapSize := scale * scaledDashSize
|
||||
return scaledDashSize, scaledGapSize
|
||||
}
|
||||
|
||||
func embedFonts(buf *bytes.Buffer) {
|
||||
func embedFonts(buf *bytes.Buffer, fontFamily *d2fonts.FontFamily) {
|
||||
content := buf.String()
|
||||
buf.WriteString(`<style type="text/css"><![CDATA[`)
|
||||
|
||||
triggers := []string{
|
||||
`class="text"`,
|
||||
`class="text `,
|
||||
`class="md"`,
|
||||
}
|
||||
|
||||
|
|
@ -875,7 +953,35 @@ func embedFonts(buf *bytes.Buffer) {
|
|||
font-family: font-regular;
|
||||
src: url("%s");
|
||||
}`,
|
||||
d2fonts.FontEncodings[d2fonts.SourceSansPro.Font(0, d2fonts.FONT_STYLE_REGULAR)])
|
||||
d2fonts.FontEncodings[fontFamily.Font(0, d2fonts.FONT_STYLE_REGULAR)])
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
triggers = []string{
|
||||
`text-underline`,
|
||||
}
|
||||
|
||||
for _, t := range triggers {
|
||||
if strings.Contains(content, t) {
|
||||
buf.WriteString(`
|
||||
.text-underline {
|
||||
text-decoration: underline;
|
||||
}`)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
triggers = []string{
|
||||
`appendix-icon`,
|
||||
}
|
||||
|
||||
for _, t := range triggers {
|
||||
if strings.Contains(content, t) {
|
||||
buf.WriteString(`
|
||||
.appendix-icon {
|
||||
filter: drop-shadow(0px 0px 32px rgba(31, 36, 58, 0.1));
|
||||
}`)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
|
@ -896,7 +1002,7 @@ func embedFonts(buf *bytes.Buffer) {
|
|||
font-family: font-bold;
|
||||
src: url("%s");
|
||||
}`,
|
||||
d2fonts.FontEncodings[d2fonts.SourceSansPro.Font(0, d2fonts.FONT_STYLE_BOLD)])
|
||||
d2fonts.FontEncodings[fontFamily.Font(0, d2fonts.FONT_STYLE_BOLD)])
|
||||
break
|
||||
}
|
||||
}
|
||||
|
|
@ -917,7 +1023,7 @@ func embedFonts(buf *bytes.Buffer) {
|
|||
font-family: font-italic;
|
||||
src: url("%s");
|
||||
}`,
|
||||
d2fonts.FontEncodings[d2fonts.SourceSansPro.Font(0, d2fonts.FONT_STYLE_ITALIC)])
|
||||
d2fonts.FontEncodings[fontFamily.Font(0, d2fonts.FONT_STYLE_ITALIC)])
|
||||
break
|
||||
}
|
||||
}
|
||||
|
|
@ -949,15 +1055,32 @@ func embedFonts(buf *bytes.Buffer) {
|
|||
}
|
||||
|
||||
// TODO minify output at end
|
||||
func Render(diagram *d2target.Diagram) ([]byte, error) {
|
||||
buf := &bytes.Buffer{}
|
||||
w, h := setViewbox(buf, diagram)
|
||||
func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) {
|
||||
var sketchRunner *d2sketch.Runner
|
||||
pad := DEFAULT_PADDING
|
||||
if opts != nil {
|
||||
pad = opts.Pad
|
||||
if opts.Sketch {
|
||||
var err error
|
||||
sketchRunner, err = d2sketch.InitSketchVM()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
w, h := setViewbox(buf, diagram, pad)
|
||||
|
||||
styleCSS2 := ""
|
||||
if sketchRunner != nil {
|
||||
styleCSS2 = "\n" + sketchStyleCSS
|
||||
}
|
||||
buf.WriteString(fmt.Sprintf(`<style type="text/css">
|
||||
<![CDATA[
|
||||
%s
|
||||
%s%s
|
||||
]]>
|
||||
</style>`, styleCSS))
|
||||
</style>`, styleCSS, styleCSS2))
|
||||
|
||||
hasMarkdown := false
|
||||
for _, s := range diagram.Shapes {
|
||||
|
|
@ -969,6 +1092,9 @@ func Render(diagram *d2target.Diagram) ([]byte, error) {
|
|||
if hasMarkdown {
|
||||
fmt.Fprintf(buf, `<style type="text/css">%s</style>`, mdCSS)
|
||||
}
|
||||
if sketchRunner != nil {
|
||||
fmt.Fprintf(buf, d2sketch.DefineFillPattern())
|
||||
}
|
||||
|
||||
// only define shadow filter if a shape uses it
|
||||
for _, s := range diagram.Shapes {
|
||||
|
|
@ -1003,37 +1129,38 @@ func Render(diagram *d2target.Diagram) ([]byte, error) {
|
|||
markers := map[string]struct{}{}
|
||||
for _, obj := range allObjects {
|
||||
if c, is := obj.(d2target.Connection); is {
|
||||
labelMask := drawConnection(buf, labelMaskID, c, markers, idToShape)
|
||||
labelMask, err := drawConnection(buf, labelMaskID, c, markers, idToShape, sketchRunner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if labelMask != "" {
|
||||
labelMasks = append(labelMasks, labelMask)
|
||||
}
|
||||
} else if s, is := obj.(d2target.Shape); is {
|
||||
labelMask, err := drawShape(buf, s)
|
||||
labelMask, err := drawShape(buf, s, sketchRunner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if labelMask != "" {
|
||||
labelMasks = append(labelMasks, labelMask)
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf("unknow object of type %T", obj)
|
||||
return nil, fmt.Errorf("unknown object of type %T", obj)
|
||||
}
|
||||
}
|
||||
|
||||
if len(labelMasks) > 0 {
|
||||
fmt.Fprint(buf, strings.Join([]string{
|
||||
fmt.Sprintf(`<mask id="%s" maskUnits="userSpaceOnUse" x="0" y="0" width="%d" height="%d">`,
|
||||
labelMaskID, w, h,
|
||||
),
|
||||
fmt.Sprintf(`<rect x="0" y="0" width="%d" height="%d" fill="white"></rect>`,
|
||||
w,
|
||||
h,
|
||||
),
|
||||
strings.Join(labelMasks, "\n"),
|
||||
`</mask>`,
|
||||
}, "\n"))
|
||||
}
|
||||
// Note: we always want this since we reference it on connections even if there end up being no masked labels
|
||||
fmt.Fprint(buf, strings.Join([]string{
|
||||
fmt.Sprintf(`<mask id="%s" maskUnits="userSpaceOnUse" x="%d" y="%d" width="%d" height="%d">`,
|
||||
labelMaskID, -pad, -pad, w, h,
|
||||
),
|
||||
fmt.Sprintf(`<rect x="%d" y="%d" width="%d" height="%d" fill="white"></rect>`,
|
||||
-pad, -pad, w, h,
|
||||
),
|
||||
strings.Join(labelMasks, "\n"),
|
||||
`</mask>`,
|
||||
}, "\n"))
|
||||
|
||||
embedFonts(buf)
|
||||
embedFonts(buf, diagram.FontFamily)
|
||||
|
||||
buf.WriteString(`</svg>`)
|
||||
return buf.Bytes(), nil
|
||||
|
|
|
|||
1
d2renderers/d2svg/github-markdown.css
vendored
|
|
@ -257,6 +257,7 @@
|
|||
margin-bottom: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 1.25;
|
||||
font-family: "font-regular";
|
||||
}
|
||||
|
||||
.md h2 {
|
||||
|
|
|
|||
12
d2renderers/d2svg/link.svg
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_3440_35088111)">
|
||||
<path d="M16 31.1109C24.3456 31.1109 31.1111 24.3454 31.1111 15.9998C31.1111 7.65415 24.3456 0.888672 16 0.888672C7.65436 0.888672 0.888885 7.65415 0.888885 15.9998C0.888885 24.3454 7.65436 31.1109 16 31.1109Z" fill="white" stroke="#DEE1EB"/>
|
||||
<path d="M14.3909 16.7965C14.7364 17.2584 15.1772 17.6406 15.6834 17.9171C16.1896 18.1938 16.7494 18.3582 17.3248 18.3993C17.9001 18.4405 18.4777 18.3575 19.0181 18.1559C19.5586 17.9543 20.0492 17.6389 20.4571 17.2309L22.8708 14.8173C23.6036 14.0586 24.0089 13.0425 23.9998 11.9877C23.9906 10.933 23.5676 9.92404 22.8217 9.17821C22.0759 8.43237 21.067 8.00931 20.0123 8.00015C18.9575 7.99098 17.9413 8.39644 17.1827 9.1292L15.7988 10.505" stroke="#2E3346" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M17.609 15.1874C17.2635 14.7255 16.8227 14.3433 16.3165 14.0667C15.8103 13.7902 15.2505 13.6257 14.6752 13.5845C14.0998 13.5433 13.5223 13.6263 12.9819 13.8279C12.4414 14.0295 11.9506 14.345 11.5428 14.753L9.1292 17.1666C8.39644 17.9252 7.99098 18.9414 8.00015 19.9962C8.00931 21.0509 8.43237 22.0598 9.17821 22.8056C9.92405 23.5515 10.933 23.9745 11.9877 23.9837C13.0425 23.9928 14.0586 23.5875 14.8173 22.8547L16.193 21.4788" stroke="#2E3346" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_3440_35088111">
|
||||
<rect width="32" height="32" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
4
d2renderers/d2svg/sketchstyle.css
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
.sketch-overlay {
|
||||
fill: url(#streaks);
|
||||
mix-blend-mode: overlay;
|
||||
}
|
||||
|
|
@ -3,23 +3,24 @@ package d2svg
|
|||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"strings"
|
||||
|
||||
"oss.terrastruct.com/d2/d2target"
|
||||
"oss.terrastruct.com/d2/lib/geo"
|
||||
"oss.terrastruct.com/d2/lib/label"
|
||||
"oss.terrastruct.com/d2/lib/svg"
|
||||
"oss.terrastruct.com/util-go/go2"
|
||||
)
|
||||
|
||||
func tableHeader(box *geo.Box, text string, textWidth, textHeight, fontSize float64) string {
|
||||
func tableHeader(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, "#0a0f25")
|
||||
box.TopLeft.X, box.TopLeft.Y, box.Width, box.Height, shape.Fill)
|
||||
|
||||
if text != "" {
|
||||
tl := label.InsideMiddleLeft.GetPointOnBox(
|
||||
box,
|
||||
20,
|
||||
textWidth,
|
||||
float64(d2target.HeaderPadding),
|
||||
float64(shape.Width),
|
||||
textHeight,
|
||||
)
|
||||
|
||||
|
|
@ -30,73 +31,54 @@ func tableHeader(box *geo.Box, text string, textWidth, textHeight, fontSize floa
|
|||
fmt.Sprintf("text-anchor:%s;font-size:%vpx;fill:%s",
|
||||
"start",
|
||||
4+fontSize,
|
||||
"white",
|
||||
shape.Stroke,
|
||||
),
|
||||
escapeText(text),
|
||||
svg.EscapeText(text),
|
||||
)
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
func tableRow(box *geo.Box, nameText, typeText, constraintText string, fontSize, longestNameWidth float64) string {
|
||||
func tableRow(shape d2target.Shape, box *geo.Box, nameText, typeText, constraintText string, fontSize, longestNameWidth float64) string {
|
||||
// Row is made up of name, type, and constraint
|
||||
// e.g. | diagram int FK |
|
||||
nameTL := label.InsideMiddleLeft.GetPointOnBox(
|
||||
box,
|
||||
prefixPadding,
|
||||
d2target.NamePadding,
|
||||
box.Width,
|
||||
fontSize,
|
||||
)
|
||||
constraintTR := label.InsideMiddleRight.GetPointOnBox(
|
||||
box,
|
||||
typePadding,
|
||||
d2target.TypePadding,
|
||||
0,
|
||||
fontSize,
|
||||
)
|
||||
|
||||
// TODO theme based
|
||||
primaryColor := "rgb(13, 50, 178)"
|
||||
accentColor := "rgb(74, 111, 243)"
|
||||
neutralColor := "rgb(103, 108, 126)"
|
||||
|
||||
return strings.Join([]string{
|
||||
fmt.Sprintf(`<text class="text" x="%f" y="%f" style="%s">%s</text>`,
|
||||
nameTL.X,
|
||||
nameTL.Y+fontSize*3/4,
|
||||
fmt.Sprintf("text-anchor:%s;font-size:%vpx;fill:%s", "start", fontSize, primaryColor),
|
||||
escapeText(nameText),
|
||||
fmt.Sprintf("text-anchor:%s;font-size:%vpx;fill:%s", "start", fontSize, shape.PrimaryAccentColor),
|
||||
svg.EscapeText(nameText),
|
||||
),
|
||||
|
||||
// TODO light font
|
||||
fmt.Sprintf(`<text class="text" x="%f" y="%f" style="%s">%s</text>`,
|
||||
nameTL.X+longestNameWidth,
|
||||
nameTL.X+longestNameWidth+2*d2target.NamePadding,
|
||||
nameTL.Y+fontSize*3/4,
|
||||
fmt.Sprintf("text-anchor:%s;font-size:%vpx;fill:%s", "start", fontSize, neutralColor),
|
||||
escapeText(typeText),
|
||||
fmt.Sprintf("text-anchor:%s;font-size:%vpx;fill:%s", "start", fontSize, shape.NeutralAccentColor),
|
||||
svg.EscapeText(typeText),
|
||||
),
|
||||
|
||||
fmt.Sprintf(`<text class="text" x="%f" y="%f" style="%s">%s</text>`,
|
||||
constraintTR.X,
|
||||
constraintTR.Y+fontSize*3/4,
|
||||
fmt.Sprintf("text-anchor:%s;font-size:%vpx;fill:%s;letter-spacing:2px;", "end", fontSize, accentColor),
|
||||
fmt.Sprintf("text-anchor:%s;font-size:%vpx;fill:%s;letter-spacing:2px;", "end", fontSize, shape.SecondaryAccentColor),
|
||||
constraintText,
|
||||
),
|
||||
}, "\n")
|
||||
}
|
||||
|
||||
func constraintAbbr(constraint string) string {
|
||||
switch constraint {
|
||||
case "primary_key":
|
||||
return "PK"
|
||||
case "foreign_key":
|
||||
return "FK"
|
||||
case "unique":
|
||||
return "UNQ"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func drawTable(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, shapeStyle(targetShape))
|
||||
|
|
@ -110,27 +92,26 @@ func drawTable(writer io.Writer, targetShape d2target.Shape) {
|
|||
headerBox := geo.NewBox(box.TopLeft, box.Width, rowHeight)
|
||||
|
||||
fmt.Fprint(writer,
|
||||
tableHeader(headerBox, targetShape.Label, float64(targetShape.LabelWidth), float64(targetShape.LabelHeight), float64(targetShape.FontSize)),
|
||||
tableHeader(targetShape, headerBox, targetShape.Label,
|
||||
float64(targetShape.LabelWidth), float64(targetShape.LabelHeight), float64(targetShape.FontSize)),
|
||||
)
|
||||
|
||||
fontSize := float64(targetShape.FontSize)
|
||||
var longestNameWidth float64
|
||||
for _, f := range targetShape.SQLTable.Columns {
|
||||
// TODO measure text
|
||||
longestNameWidth = math.Max(longestNameWidth, float64(len(f.Name))*fontSize*5/9)
|
||||
var longestNameWidth int
|
||||
for _, f := range targetShape.Columns {
|
||||
longestNameWidth = go2.Max(longestNameWidth, f.Name.LabelWidth)
|
||||
}
|
||||
|
||||
rowBox := geo.NewBox(box.TopLeft.Copy(), box.Width, rowHeight)
|
||||
rowBox.TopLeft.Y += headerBox.Height
|
||||
for _, f := range targetShape.SQLTable.Columns {
|
||||
for _, f := range targetShape.Columns {
|
||||
fmt.Fprint(writer,
|
||||
tableRow(rowBox, f.Name, f.Type, constraintAbbr(f.Constraint), fontSize, longestNameWidth),
|
||||
tableRow(targetShape, rowBox, f.Name.Label, f.Type.Label, f.ConstraintAbbr(), float64(targetShape.FontSize), float64(longestNameWidth)),
|
||||
)
|
||||
rowBox.TopLeft.Y += rowHeight
|
||||
fmt.Fprintf(writer, `<line x1="%f" y1="%f" x2="%f" y2="%f" style="stroke-width:2;stroke:#0a0f25" />`,
|
||||
fmt.Fprintf(writer, `<line x1="%f" y1="%f" x2="%f" y2="%f" style="stroke-width:2;stroke:%s" />`,
|
||||
rowBox.TopLeft.X, rowBox.TopLeft.Y,
|
||||
rowBox.TopLeft.X+rowBox.Width, rowBox.TopLeft.Y,
|
||||
targetShape.Fill,
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
13
d2renderers/d2svg/tooltip.svg
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_3427_35082111)">
|
||||
<path d="M16 31.1109C24.3456 31.1109 31.1111 24.3454 31.1111 15.9998C31.1111 7.65415 24.3456 0.888672 16 0.888672C7.65436 0.888672 0.888885 7.65415 0.888885 15.9998C0.888885 24.3454 7.65436 31.1109 16 31.1109Z" fill="white" stroke="#DEE1EB"/>
|
||||
<path d="M16 26C21.5228 26 26 21.5228 26 16C26 10.4772 21.5228 6 16 6C10.4772 6 6 10.4772 6 16C6 21.5228 10.4772 26 16 26Z" stroke="#2E3346" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M16 19.998V15.998" stroke="#2E3346" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M16 12H16.0098" stroke="#2E3346" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_3427_35082111">
|
||||
<rect width="32" height="32" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 926 B |
|
|
@ -6,6 +6,11 @@ import (
|
|||
"oss.terrastruct.com/d2/d2renderers/d2fonts"
|
||||
)
|
||||
|
||||
const (
|
||||
PrefixPadding = 10
|
||||
PrefixWidth = 20
|
||||
)
|
||||
|
||||
type Class struct {
|
||||
Fields []ClassField `json:"fields"`
|
||||
Methods []ClassMethod `json:"methods"`
|
||||
|
|
@ -27,6 +32,17 @@ func (cf ClassField) Text() *MText {
|
|||
}
|
||||
}
|
||||
|
||||
func (cf ClassField) VisibilityToken() string {
|
||||
switch cf.Visibility {
|
||||
case "protected":
|
||||
return "#"
|
||||
case "private":
|
||||
return "-"
|
||||
default:
|
||||
return "+"
|
||||
}
|
||||
}
|
||||
|
||||
type ClassMethod struct {
|
||||
Name string `json:"name"`
|
||||
Return string `json:"return"`
|
||||
|
|
@ -42,3 +58,14 @@ func (cm ClassMethod) Text() *MText {
|
|||
Shape: "class",
|
||||
}
|
||||
}
|
||||
|
||||
func (cm ClassMethod) VisibilityToken() string {
|
||||
switch cm.Visibility {
|
||||
case "protected":
|
||||
return "#"
|
||||
case "private":
|
||||
return "-"
|
||||
default:
|
||||
return "+"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import (
|
|||
|
||||
"oss.terrastruct.com/util-go/go2"
|
||||
|
||||
"oss.terrastruct.com/d2/d2renderers/d2fonts"
|
||||
"oss.terrastruct.com/d2/d2themes"
|
||||
"oss.terrastruct.com/d2/lib/geo"
|
||||
"oss.terrastruct.com/d2/lib/label"
|
||||
|
|
@ -22,8 +23,9 @@ const (
|
|||
)
|
||||
|
||||
type Diagram struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
FontFamily *d2fonts.FontFamily `json:"fontFamily,omitempty"`
|
||||
|
||||
Shapes []Shape `json:"shapes"`
|
||||
Connections []Connection `json:"connections"`
|
||||
|
|
@ -144,6 +146,11 @@ type Shape struct {
|
|||
|
||||
ZIndex int `json:"zIndex"`
|
||||
Level int `json:"level"`
|
||||
|
||||
// These are used for special shapes, sql_table and class
|
||||
PrimaryAccentColor string `json:"primaryAccentColor,omitempty"`
|
||||
SecondaryAccentColor string `json:"secondaryAccentColor,omitempty"`
|
||||
NeutralAccentColor string `json:"neutralAccentColor,omitempty"`
|
||||
}
|
||||
|
||||
func (s *Shape) SetType(t string) {
|
||||
|
|
@ -207,6 +214,7 @@ type Connection struct {
|
|||
StrokeDash float64 `json:"strokeDash"`
|
||||
StrokeWidth int `json:"strokeWidth"`
|
||||
Stroke string `json:"stroke"`
|
||||
Fill string `json:"fill,omitempty"`
|
||||
|
||||
Text
|
||||
LabelPosition string `json:"labelPosition"`
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
package d2target
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
import "oss.terrastruct.com/d2/d2renderers/d2fonts"
|
||||
|
||||
"oss.terrastruct.com/d2/d2renderers/d2fonts"
|
||||
const (
|
||||
NamePadding = 10
|
||||
TypePadding = 20
|
||||
HeaderPadding = 20
|
||||
)
|
||||
|
||||
type SQLTable struct {
|
||||
|
|
@ -11,18 +13,40 @@ type SQLTable struct {
|
|||
}
|
||||
|
||||
type SQLColumn struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Name Text `json:"name"`
|
||||
Type Text `json:"type"`
|
||||
Constraint string `json:"constraint"`
|
||||
Reference string `json:"reference"`
|
||||
}
|
||||
|
||||
func (c SQLColumn) Text() *MText {
|
||||
return &MText{
|
||||
Text: fmt.Sprintf("%s%s%s%s", c.Name, c.Type, c.Constraint, c.Reference),
|
||||
FontSize: d2fonts.FONT_SIZE_L,
|
||||
IsBold: false,
|
||||
IsItalic: false,
|
||||
Shape: "sql_table",
|
||||
func (c SQLColumn) Texts() []*MText {
|
||||
return []*MText{
|
||||
{
|
||||
Text: c.Name.Label,
|
||||
FontSize: d2fonts.FONT_SIZE_L,
|
||||
IsBold: false,
|
||||
IsItalic: false,
|
||||
Shape: "sql_table",
|
||||
},
|
||||
{
|
||||
Text: c.Type.Label,
|
||||
FontSize: d2fonts.FONT_SIZE_L,
|
||||
IsBold: false,
|
||||
IsItalic: false,
|
||||
Shape: "sql_table",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c SQLColumn) ConstraintAbbr() string {
|
||||
switch c.Constraint {
|
||||
case "primary_key":
|
||||
return "PK"
|
||||
case "foreign_key":
|
||||
return "FK"
|
||||
case "unique":
|
||||
return "UNQ"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,14 @@
|
|||
# Contributing
|
||||
|
||||
<!-- toc -->
|
||||
|
||||
- [CI](#ci)
|
||||
- [Flow](#flow)
|
||||
- [Logistics](#logistics)
|
||||
- [Dev](#dev)
|
||||
* [Content](#content)
|
||||
* [Tests](#tests)
|
||||
+ [Running tests](#running-tests)
|
||||
+ [Chaos tests](#chaos-tests)
|
||||
* [Documentation](#documentation)
|
||||
* [Questions](#questions)
|
||||
|
||||
<!-- tocstop -->
|
||||
- <a href="#ci" id="toc-ci">CI</a>
|
||||
- <a href="#flow" id="toc-flow">Flow</a>
|
||||
- <a href="#logistics" id="toc-logistics">Logistics</a>
|
||||
- <a href="#dev" id="toc-dev">Dev</a>
|
||||
- <a href="#content" id="toc-content">Content</a>
|
||||
- <a href="#tests" id="toc-tests">Tests</a>
|
||||
- <a href="#documentation" id="toc-documentation">Documentation</a>
|
||||
- <a href="#questions" id="toc-questions">Questions</a>
|
||||
|
||||
## CI
|
||||
|
||||
|
|
@ -44,12 +39,6 @@ The simplified D2 flow at a package level looks like:
|
|||
|
||||
## Logistics
|
||||
|
||||
- **Important**: Contributions to D2 require a CLA. We will never relicense D2, but we
|
||||
need to retain full copyright for any modifications we might need to make in our
|
||||
commercial offerings. Please email cla@terrastruct.com with your name and Github
|
||||
username stating that you agree to [Terrastruct's
|
||||
CLA](https://terrastruct-site-assets.s3.us-west-1.amazonaws.com/documents/terrastruct_cla.pdf).
|
||||
You only have to do this the first time you contribute.
|
||||
- D2 uses Issues as TODOs. No auto-closing on staleness.
|
||||
- Branch off `master`.
|
||||
- Prefix pull request titles with a short descriptor of the domain, e.g. `d2renderer: Add
|
||||
|
|
|
|||
|
|
@ -6,14 +6,17 @@ You may install `d2` through any of the following methods.
|
|||
- <a href="#installsh" id="toc-installsh">install.sh</a>
|
||||
- <a href="#security" id="toc-security">Security</a>
|
||||
- <a href="#macos-homebrew" id="toc-macos-homebrew">macOS (Homebrew)</a>
|
||||
- <a href="#linux" id="toc-linux">Linux</a>
|
||||
- <a href="#void-linux" id="toc-void-linux">Void Linux</a>
|
||||
- <a href="#standalone" id="toc-standalone">Standalone</a>
|
||||
- <a href="#manual" id="toc-manual">Manual</a>
|
||||
- <a href="#prefix" id="toc-prefix">PREFIX</a>
|
||||
- <a href="#from-source" id="toc-from-source">From source</a>
|
||||
- <a href="#source-release" id="toc-source-release">Source Release</a>
|
||||
- <a href="#windows" id="toc-windows">Windows</a>
|
||||
- <a href="#msys2" id="toc-msys2">MSYS2</a>
|
||||
- <a href="#release-archives" id="toc-release-archives">Release archives</a>
|
||||
- <a href="#wsl" id="toc-wsl">WSL</a>
|
||||
- <a href="#docker" id="toc-docker">Docker</a>
|
||||
- <a href="#coming-soon" id="toc-coming-soon">Coming soon</a>
|
||||
|
||||
## install.sh
|
||||
|
|
@ -71,13 +74,30 @@ but that is coming soon. [#315](https://github.com/terrastruct/d2/issues/315)
|
|||
If you're on macOS, you can install with `brew`.
|
||||
|
||||
```sh
|
||||
brew tap terrastruct/d2
|
||||
brew install d2
|
||||
```
|
||||
|
||||
> The install script above does this automatically if you have `brew` installed and
|
||||
> are running it on macOS.
|
||||
|
||||
You can also install from source with:
|
||||
|
||||
```d2
|
||||
brew install d2 --HEAD
|
||||
```
|
||||
|
||||
## Linux
|
||||
|
||||
The following distributions have packages for d2:
|
||||
|
||||
### Void Linux
|
||||
|
||||
All supported platforms:
|
||||
|
||||
```sh
|
||||
xbps-install d2
|
||||
```
|
||||
|
||||
## Standalone
|
||||
|
||||
We publish standalone release archives for every release on Github.
|
||||
|
|
@ -138,9 +158,11 @@ You can always install from source:
|
|||
go install oss.terrastruct.com/d2@latest
|
||||
```
|
||||
|
||||
You need at least Go v1.18
|
||||
|
||||
### Source Release
|
||||
|
||||
To install a proper release from source clone the repository and then:
|
||||
To install a release from source clone the repository and then:
|
||||
|
||||
```sh
|
||||
./ci/release/build.sh --install
|
||||
|
|
@ -149,35 +171,30 @@ To install a proper release from source clone the repository and then:
|
|||
```
|
||||
|
||||
Installing a real release will also install manpages and in the future other assets like
|
||||
fonts and icons. Furthermore, when installing a non versioned commit, installing a proper
|
||||
release will ensure that `d2 --version` works correctly by embedding the commit hash into
|
||||
the `d2` binary.
|
||||
fonts and icons. Furthermore, when installing a non versioned commit, installing a release
|
||||
will ensure that `d2 --version` works correctly by embedding the commit hash into the `d2`
|
||||
binary.
|
||||
|
||||
Remember, you need at least Go v1.18
|
||||
|
||||
## Windows
|
||||
|
||||
d2 builds and runs on Windows:
|
||||
We have prebuilt releases of d2 available for Windows via `.msi` installers. The installer
|
||||
will add the `d2` binary to your `$PATH` so that you can execute `d2` in `cmd.exe` or
|
||||
`pwsh.exe`.
|
||||
|
||||
We have prebuilt standalone releases for Windows though they're structured in the same way
|
||||
as our Unix releases. So after extracting a release, you'll have to manually put the d2
|
||||
binary into your `$PATH` or add the `bin` directory of the release into your `$PATH`.
|
||||
### Release archives
|
||||
|
||||
See https://www.wikihow.com/Change-the-PATH-Environment-Variable-on-Windows
|
||||
|
||||
Then you'll be able to call `d2` from the commandline in `cmd.exe` or `pwsh.exe`.
|
||||
|
||||
We intend to have a `.msi` release installer sometime soon that handles putting `d2` into
|
||||
your `$PATH` for you.
|
||||
|
||||
### MSYS2
|
||||
We also have release archives for Windows structured in the same way as our Unix releases
|
||||
for use with MSYS2.
|
||||
|
||||
<img width="1680" alt="Screenshot 2022-12-06 at 2 55 27 AM" src="https://user-images.githubusercontent.com/10180857/205892927-6f3e116c-1c4a-440a-9972-82c306aa9779.png">
|
||||
|
||||
We recommend using [MSYS2](https://www.msys2.org/) or [Git
|
||||
Bash](https://gitforwindows.org/#bash) (Git Bash is based on MSYS2) for an improved
|
||||
terminal experience.
|
||||
See [MSYS2](https://www.msys2.org/) or [Git Bash](https://gitforwindows.org/#bash) (Git
|
||||
Bash is based on MSYS2).
|
||||
|
||||
MSYS2 provides a unix style shell environment that is native to Windows (unlike
|
||||
[Cygwin](https://www.cygwin.com/)). MSYS2 allows `install.sh` to work, enables proper
|
||||
[Cygwin](https://www.cygwin.com/)). MSYS2 allows `install.sh` to work, enables automatic
|
||||
installation of our standalone releases via `make install` and makes the manpage
|
||||
accessible via `man d2`.
|
||||
|
||||
|
|
@ -192,9 +209,23 @@ under plain Windows.
|
|||
aka Windows Subsystem for Linux if that's what you prefer. Installation is just like any
|
||||
other Linux system.
|
||||
|
||||
## Docker
|
||||
|
||||
https://hub.docker.com/repository/docker/terrastruct/d2
|
||||
|
||||
We publish `amd64` and `arm64` images based on `debian:latest` for each release.
|
||||
|
||||
Example usage:
|
||||
|
||||
```sh
|
||||
echo 'x -> y' >helloworld.d2
|
||||
docker run --rm -it -u "$(id -u):$(id -g)" -v "$PWD:/root/src" \
|
||||
-p 127.0.0.1:8080:8080 terrastruct/d2:v0.1.2 --watch helloworld.d2
|
||||
# Visit http://127.0.0.1:8080
|
||||
```
|
||||
|
||||
## Coming soon
|
||||
|
||||
- Docker image
|
||||
- rpm and deb packages
|
||||
- with repositories and standalone
|
||||
- homebrew core
|
||||
|
|
|
|||
BIN
docs/assets/playground_button.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
|
|
@ -20,6 +20,8 @@ func main() {
|
|||
Ruler: ruler,
|
||||
ThemeID: d2themescatalog.GrapeSoda.ID,
|
||||
})
|
||||
out, _ := d2svg.Render(diagram)
|
||||
out, _ := d2svg.Render(diagram, &d2svg.RenderOpts{
|
||||
Pad: d2svg.DEFAULT_PADDING,
|
||||
})
|
||||
_ = ioutil.WriteFile(filepath.Join("out.svg"), out, 0600)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,9 +18,11 @@ import (
|
|||
func main() {
|
||||
graph, _ := d2compiler.Compile("", strings.NewReader("x -> y"), nil)
|
||||
ruler, _ := textmeasure.NewRuler()
|
||||
_ = graph.SetDimensions(nil, ruler)
|
||||
_ = graph.SetDimensions(nil, ruler, nil)
|
||||
_ = d2dagrelayout.Layout(context.Background(), graph)
|
||||
diagram, _ := d2exporter.Export(context.Background(), graph, d2themescatalog.NeutralDefault.ID)
|
||||
out, _ := d2svg.Render(diagram)
|
||||
diagram, _ := d2exporter.Export(context.Background(), graph, d2themescatalog.NeutralDefault.ID, nil)
|
||||
out, _ := d2svg.Render(diagram, &d2svg.RenderOpts{
|
||||
Pad: d2svg.DEFAULT_PADDING,
|
||||
})
|
||||
_ = ioutil.WriteFile(filepath.Join("out.svg"), out, 0600)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ D2 is built to be hackable -- the language has an API built on top of it to make
|
|||
programmatically.
|
||||
|
||||
Modifying the previous example, this example demonstrates how
|
||||
[d2oracle](../../../d2oracle) can be used to create a new shape, style it programatically
|
||||
[d2oracle](../../../d2oracle) can be used to create a new shape, style it programmatically
|
||||
and then output the modified d2 script.
|
||||
|
||||
This makes it easy to build functionality on top of D2. Terrastruct uses the
|
||||
|
|
@ -27,4 +27,4 @@ visual interface.
|
|||
`d2lib` from the first example is just a wrapper around the lower level APIs. They
|
||||
can be used directly and this example demonstrates such usage.
|
||||
|
||||
This shouldn't be necessary for most usecases.
|
||||
This shouldn't be necessary for most use cases.
|
||||
|
|
|
|||
|
|
@ -17,9 +17,12 @@ import (
|
|||
"oss.terrastruct.com/util-go/assert"
|
||||
"oss.terrastruct.com/util-go/diff"
|
||||
|
||||
"oss.terrastruct.com/d2/d2compiler"
|
||||
"oss.terrastruct.com/d2/d2graph"
|
||||
"oss.terrastruct.com/d2/d2layouts/d2dagrelayout"
|
||||
"oss.terrastruct.com/d2/d2layouts/d2elklayout"
|
||||
"oss.terrastruct.com/d2/d2layouts/d2near"
|
||||
"oss.terrastruct.com/d2/d2layouts/d2sequence"
|
||||
"oss.terrastruct.com/d2/d2lib"
|
||||
"oss.terrastruct.com/d2/d2renderers/d2svg"
|
||||
"oss.terrastruct.com/d2/d2target"
|
||||
|
|
@ -88,6 +91,28 @@ func runa(t *testing.T, tcs []testCase) {
|
|||
}
|
||||
}
|
||||
|
||||
// serde exercises serializing and deserializing the graph
|
||||
// We want to run all the steps leading up to serialization in the course of regular layout
|
||||
func serde(t *testing.T, tc testCase, ruler *textmeasure.Ruler) {
|
||||
ctx := context.Background()
|
||||
ctx = log.WithTB(ctx, t, nil)
|
||||
g, err := d2compiler.Compile("", strings.NewReader(tc.script), &d2compiler.CompileOptions{
|
||||
UTF16: false,
|
||||
})
|
||||
tassert.Nil(t, err)
|
||||
if len(g.Objects) > 0 {
|
||||
err = g.SetDimensions(nil, ruler, nil)
|
||||
tassert.Nil(t, err)
|
||||
d2near.WithoutConstantNears(ctx, g)
|
||||
d2sequence.WithoutSequenceDiagrams(ctx, g)
|
||||
}
|
||||
b, err := d2graph.SerializeGraph(g)
|
||||
tassert.Nil(t, err)
|
||||
var newG d2graph.Graph
|
||||
err = d2graph.DeserializeGraph(b, &newG)
|
||||
tassert.Nil(t, err)
|
||||
}
|
||||
|
||||
func run(t *testing.T, tc testCase) {
|
||||
ctx := context.Background()
|
||||
ctx = log.WithTB(ctx, t, nil)
|
||||
|
|
@ -98,6 +123,8 @@ func run(t *testing.T, tc testCase) {
|
|||
return
|
||||
}
|
||||
|
||||
serde(t, tc, ruler)
|
||||
|
||||
layoutsTested := []string{"dagre", "elk"}
|
||||
|
||||
for _, layoutName := range layoutsTested {
|
||||
|
|
@ -125,14 +152,21 @@ func run(t *testing.T, tc testCase) {
|
|||
dataPath := filepath.Join("testdata", strings.TrimPrefix(t.Name(), "TestE2E/"), layoutName)
|
||||
pathGotSVG := filepath.Join(dataPath, "sketch.got.svg")
|
||||
|
||||
svgBytes, err := d2svg.Render(diagram)
|
||||
svgBytes, err := d2svg.Render(diagram, &d2svg.RenderOpts{
|
||||
Pad: d2svg.DEFAULT_PADDING,
|
||||
})
|
||||
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)
|
||||
// if running from e2ereport.sh, we want to keep .got.svg on a failure
|
||||
forReport := os.Getenv("E2E_REPORT") != ""
|
||||
if !forReport {
|
||||
defer os.Remove(pathGotSVG)
|
||||
}
|
||||
|
||||
// Check that it's valid SVG
|
||||
var xmlParsed interface{}
|
||||
err = xml.Unmarshal(svgBytes, &xmlParsed)
|
||||
assert.Success(t, err)
|
||||
|
|
@ -143,6 +177,9 @@ func run(t *testing.T, tc testCase) {
|
|||
err = diff.Testdata(filepath.Join(dataPath, "sketch"), ".svg", svgBytes)
|
||||
assert.Success(t, err)
|
||||
}
|
||||
if forReport {
|
||||
os.Remove(pathGotSVG)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -34,6 +34,255 @@ A->B`,
|
|||
script: `shape: sequence_diagram
|
||||
b.1 -> b.1
|
||||
b.1 -> b.1`,
|
||||
}, {
|
||||
name: "sequence_diagram_no_message",
|
||||
script: `shape: sequence_diagram
|
||||
a: A
|
||||
b: B`,
|
||||
},
|
||||
{
|
||||
name: "sequence_diagram_name_crash",
|
||||
script: `foo: {
|
||||
shape: sequence_diagram
|
||||
a -> b
|
||||
}
|
||||
foobar: {
|
||||
shape: sequence_diagram
|
||||
c -> d
|
||||
}
|
||||
foo -> foobar`,
|
||||
},
|
||||
{
|
||||
name: "sql_table_overflow",
|
||||
script: `
|
||||
table: sql_table_overflow {
|
||||
shape: sql_table
|
||||
short: loooooooooooooooooooong
|
||||
loooooooooooooooooooong: short
|
||||
}
|
||||
table_constrained: sql_table_constrained_overflow {
|
||||
shape: sql_table
|
||||
short: loooooooooooooooooooong {
|
||||
constraint: unique
|
||||
}
|
||||
loooooooooooooooooooong: short {
|
||||
constraint: foreign_key
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "elk_alignment",
|
||||
script: `
|
||||
direction: down
|
||||
|
||||
build_workflow: lambda-build.yaml {
|
||||
|
||||
push: Push to main branch {
|
||||
style.font-size: 25
|
||||
}
|
||||
|
||||
GHA: GitHub Actions {
|
||||
style.font-size: 25
|
||||
}
|
||||
|
||||
S3.style.font-size: 25
|
||||
Terraform.style.font-size: 25
|
||||
AWS.style.font-size: 25
|
||||
|
||||
push -> GHA: Triggers {
|
||||
style.font-size: 20
|
||||
}
|
||||
|
||||
GHA -> S3: Builds zip and pushes it {
|
||||
style.font-size: 20
|
||||
}
|
||||
|
||||
S3 <-> Terraform: Pulls zip to deploy {
|
||||
style.font-size: 20
|
||||
}
|
||||
|
||||
Terraform -> AWS: Changes live lambdas {
|
||||
style.font-size: 20
|
||||
}
|
||||
}
|
||||
|
||||
deploy_workflow: lambda-deploy.yaml {
|
||||
|
||||
manual: Manual Trigger {
|
||||
style.font-size: 25
|
||||
}
|
||||
|
||||
GHA: GitHub Actions {
|
||||
style.font-size: 25
|
||||
}
|
||||
|
||||
AWS.style.font-size: 25
|
||||
|
||||
Manual -> GHA: Launches {
|
||||
style.font-size: 20
|
||||
}
|
||||
|
||||
GHA -> AWS: Builds zip\npushes them to S3.\n\nDeploys lambdas\nusing Terraform {
|
||||
style.font-size: 20
|
||||
}
|
||||
}
|
||||
|
||||
apollo_workflow: apollo-deploy.yaml {
|
||||
|
||||
apollo: Apollo Repo {
|
||||
style.font-size: 25
|
||||
}
|
||||
|
||||
GHA: GitHub Actions {
|
||||
style.font-size: 25
|
||||
}
|
||||
|
||||
AWS.style.font-size: 25
|
||||
|
||||
apollo -> GHA: Triggered manually/push to master test test test test test test test {
|
||||
style.font-size: 20
|
||||
}
|
||||
|
||||
GHA -> AWS: test {
|
||||
style.font-size: 20
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "dagre_edge_label_spacing",
|
||||
script: `direction: right
|
||||
|
||||
build_workflow: lambda-build.yaml {
|
||||
|
||||
push: Push to main branch {
|
||||
style.font-size: 25
|
||||
}
|
||||
GHA: GitHub Actions {
|
||||
style.font-size: 25
|
||||
}
|
||||
S3.style.font-size: 25
|
||||
Terraform.style.font-size: 25
|
||||
AWS.style.font-size: 25
|
||||
|
||||
push -> GHA: Triggers
|
||||
GHA -> S3: Builds zip & pushes it
|
||||
S3 <-> Terraform: Pulls zip to deploy
|
||||
Terraform -> AWS: Changes the live lambdas
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "query_param_escape",
|
||||
script: `my network: {
|
||||
icon: https://icons.terrastruct.com/infra/019-network.svg?fuga=1&hoge
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "elk_order",
|
||||
script: `queue: {
|
||||
shape: queue
|
||||
label: ''
|
||||
|
||||
M0
|
||||
M1
|
||||
M2
|
||||
M3
|
||||
M4
|
||||
M5
|
||||
M6
|
||||
}
|
||||
|
||||
m0_desc: |md
|
||||
Oldest message
|
||||
|
|
||||
m0_desc -> queue.M0
|
||||
|
||||
m2_desc: |md
|
||||
Offset
|
||||
|
|
||||
m2_desc -> queue.M2
|
||||
|
||||
m5_desc: |md
|
||||
Last message
|
||||
|
|
||||
m5_desc -> queue.M5
|
||||
|
||||
m6_desc: |md
|
||||
Next message will be\
|
||||
inserted here
|
||||
|
|
||||
m6_desc -> queue.M6
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "unnamed_class_table_code",
|
||||
script: `
|
||||
|
||||
class -> users -> code
|
||||
|
||||
class: "" {
|
||||
shape: class
|
||||
-num: int
|
||||
-timeout: int
|
||||
-pid
|
||||
|
||||
+getStatus(): Enum
|
||||
+getJobs(): "Job[]"
|
||||
+setTimeout(seconds int)
|
||||
}
|
||||
|
||||
users: "" {
|
||||
shape: sql_table
|
||||
id: int
|
||||
name: string
|
||||
email: string
|
||||
password: string
|
||||
last_login: datetime
|
||||
}
|
||||
|
||||
code: |go
|
||||
a := 5
|
||||
b := a + 7
|
||||
fmt.Printf("%d", b)
|
||||
|
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "elk_img_empty_label_panic",
|
||||
script: `
|
||||
img: {
|
||||
label: ""
|
||||
shape: image
|
||||
icon: https://icons.terrastruct.com/infra/019-network.svg
|
||||
}
|
||||
ico: {
|
||||
label: ""
|
||||
icon: https://icons.terrastruct.com/infra/019-network.svg
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "only_header_class_table",
|
||||
script: `
|
||||
|
||||
class: RefreshAuthorizationPolicyProtocolServerSideTranslatorProtocolBuffer {
|
||||
shape: class
|
||||
}
|
||||
|
||||
table: RefreshAuthorizationPolicyCache {
|
||||
shape: sql_table
|
||||
}
|
||||
|
||||
table with short col: RefreshAuthorizationPolicyCache {
|
||||
shape: sql_table
|
||||
ok
|
||||
}
|
||||
|
||||
class -> table -> table with short col
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -840,7 +840,7 @@ a -> md -> b
|
|||
name: string
|
||||
email: string
|
||||
password: string
|
||||
last_login: datetime
|
||||
last_login: datetime { constraint: primary_key }
|
||||
}
|
||||
|
||||
products: {
|
||||
|
|
@ -917,12 +917,13 @@ y: {
|
|||
}
|
||||
}
|
||||
|
||||
x -> y: {
|
||||
x -> y: in style {
|
||||
style: {
|
||||
stroke: green
|
||||
opacity: 0.5
|
||||
stroke-width: 2
|
||||
stroke-dash: 5
|
||||
fill: lavender
|
||||
}
|
||||
}
|
||||
`,
|
||||
|
|
@ -1041,6 +1042,7 @@ size S -> size M: custom 15 {
|
|||
}
|
||||
size XXXL -> custom 64: custom 48 {
|
||||
style.font-size: 48
|
||||
style.fill: lavender
|
||||
}
|
||||
`,
|
||||
}, {
|
||||
|
|
@ -1481,6 +1483,245 @@ a.note: "just\na\nlong\nnote\nhere"`,
|
|||
script: `shape: sequence_diagram
|
||||
alice -> bob: what does it mean to be well-adjusted
|
||||
bob -> alice: The ability to play bridge or golf as if they were games
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "markdown_stroke_fill",
|
||||
script: `
|
||||
container.md: |md
|
||||
# a header
|
||||
|
||||
a line of text and an
|
||||
|
||||
{
|
||||
indented: "block",
|
||||
of: "json",
|
||||
}
|
||||
|
||||
walk into a bar.
|
||||
| {
|
||||
style.stroke: darkorange
|
||||
}
|
||||
|
||||
container -> no container
|
||||
|
||||
no container: |md
|
||||
they did it in style
|
||||
|
|
||||
|
||||
no container.style: {
|
||||
stroke: red
|
||||
fill: "#CEEDEE"
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "overlapping_image_container_labels",
|
||||
script: `
|
||||
root: {
|
||||
shape: image
|
||||
icon: https://icons.terrastruct.com/essentials/004-picture.svg
|
||||
}
|
||||
|
||||
root -> container.root
|
||||
|
||||
container: {
|
||||
root: {
|
||||
shape: image
|
||||
icon: https://icons.terrastruct.com/essentials/004-picture.svg
|
||||
}
|
||||
|
||||
left: {
|
||||
root: {
|
||||
shape: image
|
||||
icon: https://icons.terrastruct.com/essentials/004-picture.svg
|
||||
}
|
||||
inner: {
|
||||
left: {
|
||||
shape: image
|
||||
icon: https://icons.terrastruct.com/essentials/004-picture.svg
|
||||
}
|
||||
right: {
|
||||
shape: image
|
||||
icon: https://icons.terrastruct.com/essentials/004-picture.svg
|
||||
}
|
||||
}
|
||||
root -> inner.left: {
|
||||
label: to inner left
|
||||
}
|
||||
root -> inner.right: {
|
||||
label: to inner right
|
||||
}
|
||||
}
|
||||
|
||||
right: {
|
||||
root: {
|
||||
shape: image
|
||||
icon: https://icons.terrastruct.com/essentials/004-picture.svg
|
||||
}
|
||||
inner: {
|
||||
left: {
|
||||
shape: image
|
||||
icon: https://icons.terrastruct.com/essentials/004-picture.svg
|
||||
}
|
||||
right: {
|
||||
shape: image
|
||||
icon: https://icons.terrastruct.com/essentials/004-picture.svg
|
||||
}
|
||||
}
|
||||
root -> inner.left: {
|
||||
label: to inner left
|
||||
}
|
||||
root -> inner.right: {
|
||||
label: to inner right
|
||||
}
|
||||
}
|
||||
|
||||
root -> left.root: {
|
||||
label: to left container root
|
||||
}
|
||||
|
||||
root -> right.root: {
|
||||
label: to right container root
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "constant_near_stress",
|
||||
script: `x -> y
|
||||
The top of the mountain: { shape: text; near: top-center }
|
||||
Joe: { shape: person; near: center-left }
|
||||
Donald: { shape: person; near: center-right }
|
||||
bottom: |md
|
||||
# Cats, no less liquid than their shadows, offer no angles to the wind.
|
||||
|
||||
If we can't fix it, it ain't broke.
|
||||
|
||||
Dieters live life in the fasting lane.
|
||||
| { near: bottom-center }
|
||||
i am top left: { shape: text; near: top-left }
|
||||
i am top right: { shape: text; near: top-right }
|
||||
i am bottom left: { shape: text; near: bottom-left }
|
||||
i am bottom right: { shape: text; near: bottom-right }
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "constant_near_title",
|
||||
script: `title: |md
|
||||
# A winning strategy
|
||||
| { near: top-center }
|
||||
|
||||
poll the people -> results
|
||||
results -> unfavorable -> poll the people
|
||||
results -> favorable -> will of the people
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "text_font_sizes",
|
||||
script: `bear: { shape: text; style.font-size: 22; style.bold: true }
|
||||
mama bear: { shape: text; style.font-size: 28; style.italic: true }
|
||||
papa bear: { shape: text; style.font-size: 32; style.underline: true }
|
||||
mama bear -> bear
|
||||
papa bear -> bear
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "tooltips",
|
||||
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
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "links",
|
||||
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: "unnamed_only_width",
|
||||
script: `
|
||||
|
||||
class -> users -> code -> package -> no width
|
||||
|
||||
class: "" {
|
||||
shape: class
|
||||
-num: int
|
||||
-timeout: int
|
||||
-pid
|
||||
|
||||
+getStatus(): Enum
|
||||
+getJobs(): "Job[]"
|
||||
+setTimeout(seconds int)
|
||||
}
|
||||
|
||||
users: "" {
|
||||
shape: sql_table
|
||||
id: int
|
||||
name: string
|
||||
email: string
|
||||
password: string
|
||||
last_login: datetime
|
||||
}
|
||||
|
||||
code: |go
|
||||
a := 5
|
||||
b := a + 7
|
||||
fmt.Printf("%d", b)
|
||||
|
|
||||
|
||||
package: "" { shape: package }
|
||||
no width: ""
|
||||
|
||||
|
||||
class.width: 512
|
||||
users.width: 512
|
||||
code.width: 512
|
||||
package.width: 512
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "unnamed_only_height",
|
||||
script: `
|
||||
|
||||
class -> users -> code -> package -> no height
|
||||
|
||||
class: "" {
|
||||
shape: class
|
||||
-num: int
|
||||
-timeout: int
|
||||
-pid
|
||||
|
||||
+getStatus(): Enum
|
||||
+getJobs(): "Job[]"
|
||||
+setTimeout(seconds int)
|
||||
}
|
||||
|
||||
users: "" {
|
||||
shape: sql_table
|
||||
id: int
|
||||
name: string
|
||||
email: string
|
||||
password: string
|
||||
last_login: datetime
|
||||
}
|
||||
|
||||
code: |go
|
||||
a := 5
|
||||
b := a + 7
|
||||
fmt.Printf("%d", b)
|
||||
|
|
||||
|
||||
package: "" { shape: package }
|
||||
no height: ""
|
||||
|
||||
|
||||
class.height: 512
|
||||
users.height: 512
|
||||
code.height: 512
|
||||
package.height: 512
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
|
|
|||
440
e2etests/testdata/regression/dagre_edge_label_spacing/dagre/board.exp.json
generated
vendored
Normal file
|
|
@ -0,0 +1,440 @@
|
|||
{
|
||||
"name": "",
|
||||
"fontFamily": "SourceSansPro",
|
||||
"shapes": [
|
||||
{
|
||||
"id": "build_workflow",
|
||||
"type": "",
|
||||
"pos": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"width": 2148,
|
||||
"height": 237,
|
||||
"opacity": 1,
|
||||
"strokeDash": 0,
|
||||
"strokeWidth": 2,
|
||||
"borderRadius": 0,
|
||||
"fill": "#E3E9FD",
|
||||
"stroke": "#0D32B2",
|
||||
"shadow": false,
|
||||
"3d": false,
|
||||
"multiple": false,
|
||||
"tooltip": "",
|
||||
"link": "",
|
||||
"icon": null,
|
||||
"iconPosition": "",
|
||||
"blend": false,
|
||||
"fields": null,
|
||||
"methods": null,
|
||||
"columns": null,
|
||||
"label": "lambda-build.yaml",
|
||||
"fontSize": 28,
|
||||
"fontFamily": "DEFAULT",
|
||||
"language": "",
|
||||
"color": "#0A0F25",
|
||||
"italic": false,
|
||||
"bold": false,
|
||||
"underline": false,
|
||||
"labelWidth": 226,
|
||||
"labelHeight": 41,
|
||||
"labelPosition": "INSIDE_TOP_CENTER",
|
||||
"zIndex": 0,
|
||||
"level": 1
|
||||
},
|
||||
{
|
||||
"id": "build_workflow.push",
|
||||
"type": "",
|
||||
"pos": {
|
||||
"x": 105,
|
||||
"y": 50
|
||||
},
|
||||
"width": 330,
|
||||
"height": 137,
|
||||
"opacity": 1,
|
||||
"strokeDash": 0,
|
||||
"strokeWidth": 2,
|
||||
"borderRadius": 0,
|
||||
"fill": "#EDF0FD",
|
||||
"stroke": "#0D32B2",
|
||||
"shadow": false,
|
||||
"3d": false,
|
||||
"multiple": false,
|
||||
"tooltip": "",
|
||||
"link": "",
|
||||
"icon": null,
|
||||
"iconPosition": "",
|
||||
"blend": false,
|
||||
"fields": null,
|
||||
"methods": null,
|
||||
"columns": null,
|
||||
"label": "Push to main branch",
|
||||
"fontSize": 25,
|
||||
"fontFamily": "DEFAULT",
|
||||
"language": "",
|
||||
"color": "#0A0F25",
|
||||
"italic": false,
|
||||
"bold": true,
|
||||
"underline": false,
|
||||
"labelWidth": 230,
|
||||
"labelHeight": 37,
|
||||
"labelPosition": "INSIDE_MIDDLE_CENTER",
|
||||
"zIndex": 0,
|
||||
"level": 2
|
||||
},
|
||||
{
|
||||
"id": "build_workflow.GHA",
|
||||
"type": "",
|
||||
"pos": {
|
||||
"x": 644,
|
||||
"y": 50
|
||||
},
|
||||
"width": 269,
|
||||
"height": 137,
|
||||
"opacity": 1,
|
||||
"strokeDash": 0,
|
||||
"strokeWidth": 2,
|
||||
"borderRadius": 0,
|
||||
"fill": "#EDF0FD",
|
||||
"stroke": "#0D32B2",
|
||||
"shadow": false,
|
||||
"3d": false,
|
||||
"multiple": false,
|
||||
"tooltip": "",
|
||||
"link": "",
|
||||
"icon": null,
|
||||
"iconPosition": "",
|
||||
"blend": false,
|
||||
"fields": null,
|
||||
"methods": null,
|
||||
"columns": null,
|
||||
"label": "GitHub Actions",
|
||||
"fontSize": 25,
|
||||
"fontFamily": "DEFAULT",
|
||||
"language": "",
|
||||
"color": "#0A0F25",
|
||||
"italic": false,
|
||||
"bold": true,
|
||||
"underline": false,
|
||||
"labelWidth": 169,
|
||||
"labelHeight": 37,
|
||||
"labelPosition": "INSIDE_MIDDLE_CENTER",
|
||||
"zIndex": 0,
|
||||
"level": 2
|
||||
},
|
||||
{
|
||||
"id": "build_workflow.S3",
|
||||
"type": "",
|
||||
"pos": {
|
||||
"x": 1122,
|
||||
"y": 50
|
||||
},
|
||||
"width": 131,
|
||||
"height": 137,
|
||||
"opacity": 1,
|
||||
"strokeDash": 0,
|
||||
"strokeWidth": 2,
|
||||
"borderRadius": 0,
|
||||
"fill": "#EDF0FD",
|
||||
"stroke": "#0D32B2",
|
||||
"shadow": false,
|
||||
"3d": false,
|
||||
"multiple": false,
|
||||
"tooltip": "",
|
||||
"link": "",
|
||||
"icon": null,
|
||||
"iconPosition": "",
|
||||
"blend": false,
|
||||
"fields": null,
|
||||
"methods": null,
|
||||
"columns": null,
|
||||
"label": "S3",
|
||||
"fontSize": 25,
|
||||
"fontFamily": "DEFAULT",
|
||||
"language": "",
|
||||
"color": "#0A0F25",
|
||||
"italic": false,
|
||||
"bold": true,
|
||||
"underline": false,
|
||||
"labelWidth": 31,
|
||||
"labelHeight": 37,
|
||||
"labelPosition": "INSIDE_MIDDLE_CENTER",
|
||||
"zIndex": 0,
|
||||
"level": 2
|
||||
},
|
||||
{
|
||||
"id": "build_workflow.Terraform",
|
||||
"type": "",
|
||||
"pos": {
|
||||
"x": 1462,
|
||||
"y": 50
|
||||
},
|
||||
"width": 218,
|
||||
"height": 137,
|
||||
"opacity": 1,
|
||||
"strokeDash": 0,
|
||||
"strokeWidth": 2,
|
||||
"borderRadius": 0,
|
||||
"fill": "#EDF0FD",
|
||||
"stroke": "#0D32B2",
|
||||
"shadow": false,
|
||||
"3d": false,
|
||||
"multiple": false,
|
||||
"tooltip": "",
|
||||
"link": "",
|
||||
"icon": null,
|
||||
"iconPosition": "",
|
||||
"blend": false,
|
||||
"fields": null,
|
||||
"methods": null,
|
||||
"columns": null,
|
||||
"label": "Terraform",
|
||||
"fontSize": 25,
|
||||
"fontFamily": "DEFAULT",
|
||||
"language": "",
|
||||
"color": "#0A0F25",
|
||||
"italic": false,
|
||||
"bold": true,
|
||||
"underline": false,
|
||||
"labelWidth": 118,
|
||||
"labelHeight": 37,
|
||||
"labelPosition": "INSIDE_MIDDLE_CENTER",
|
||||
"zIndex": 0,
|
||||
"level": 2
|
||||
},
|
||||
{
|
||||
"id": "build_workflow.AWS",
|
||||
"type": "",
|
||||
"pos": {
|
||||
"x": 1889,
|
||||
"y": 50
|
||||
},
|
||||
"width": 155,
|
||||
"height": 137,
|
||||
"opacity": 1,
|
||||
"strokeDash": 0,
|
||||
"strokeWidth": 2,
|
||||
"borderRadius": 0,
|
||||
"fill": "#EDF0FD",
|
||||
"stroke": "#0D32B2",
|
||||
"shadow": false,
|
||||
"3d": false,
|
||||
"multiple": false,
|
||||
"tooltip": "",
|
||||
"link": "",
|
||||
"icon": null,
|
||||
"iconPosition": "",
|
||||
"blend": false,
|
||||
"fields": null,
|
||||
"methods": null,
|
||||
"columns": null,
|
||||
"label": "AWS",
|
||||
"fontSize": 25,
|
||||
"fontFamily": "DEFAULT",
|
||||
"language": "",
|
||||
"color": "#0A0F25",
|
||||
"italic": false,
|
||||
"bold": true,
|
||||
"underline": false,
|
||||
"labelWidth": 55,
|
||||
"labelHeight": 37,
|
||||
"labelPosition": "INSIDE_MIDDLE_CENTER",
|
||||
"zIndex": 0,
|
||||
"level": 2
|
||||
}
|
||||
],
|
||||
"connections": [
|
||||
{
|
||||
"id": "build_workflow.(push -> GHA)[0]",
|
||||
"src": "build_workflow.push",
|
||||
"srcArrow": "none",
|
||||
"srcLabel": "",
|
||||
"dst": "build_workflow.GHA",
|
||||
"dstArrow": "triangle",
|
||||
"dstLabel": "",
|
||||
"opacity": 1,
|
||||
"strokeDash": 0,
|
||||
"strokeWidth": 2,
|
||||
"stroke": "#0D32B2",
|
||||
"label": "Triggers",
|
||||
"fontSize": 16,
|
||||
"fontFamily": "DEFAULT",
|
||||
"language": "",
|
||||
"color": "#676C7E",
|
||||
"italic": true,
|
||||
"bold": false,
|
||||
"underline": false,
|
||||
"labelWidth": 54,
|
||||
"labelHeight": 21,
|
||||
"labelPosition": "INSIDE_MIDDLE_CENTER",
|
||||
"labelPercentage": 0,
|
||||
"route": [
|
||||
{
|
||||
"x": 435.5,
|
||||
"y": 118.5
|
||||
},
|
||||
{
|
||||
"x": 518.3,
|
||||
"y": 118.5
|
||||
},
|
||||
{
|
||||
"x": 559.9,
|
||||
"y": 118.5
|
||||
},
|
||||
{
|
||||
"x": 643.5,
|
||||
"y": 118.5
|
||||
}
|
||||
],
|
||||
"isCurve": true,
|
||||
"animated": false,
|
||||
"tooltip": "",
|
||||
"icon": null,
|
||||
"zIndex": 0
|
||||
},
|
||||
{
|
||||
"id": "build_workflow.(GHA -> S3)[0]",
|
||||
"src": "build_workflow.GHA",
|
||||
"srcArrow": "none",
|
||||
"srcLabel": "",
|
||||
"dst": "build_workflow.S3",
|
||||
"dstArrow": "triangle",
|
||||
"dstLabel": "",
|
||||
"opacity": 1,
|
||||
"strokeDash": 0,
|
||||
"strokeWidth": 2,
|
||||
"stroke": "#0D32B2",
|
||||
"label": "Builds zip & pushes it",
|
||||
"fontSize": 16,
|
||||
"fontFamily": "DEFAULT",
|
||||
"language": "",
|
||||
"color": "#676C7E",
|
||||
"italic": true,
|
||||
"bold": false,
|
||||
"underline": false,
|
||||
"labelWidth": 138,
|
||||
"labelHeight": 21,
|
||||
"labelPosition": "INSIDE_MIDDLE_CENTER",
|
||||
"labelPercentage": 0,
|
||||
"route": [
|
||||
{
|
||||
"x": 913.5,
|
||||
"y": 118.5
|
||||
},
|
||||
{
|
||||
"x": 996.3,
|
||||
"y": 118.5
|
||||
},
|
||||
{
|
||||
"x": 1037.9,
|
||||
"y": 118.5
|
||||
},
|
||||
{
|
||||
"x": 1121.5,
|
||||
"y": 118.5
|
||||
}
|
||||
],
|
||||
"isCurve": true,
|
||||
"animated": false,
|
||||
"tooltip": "",
|
||||
"icon": null,
|
||||
"zIndex": 0
|
||||
},
|
||||
{
|
||||
"id": "build_workflow.(S3 <-> Terraform)[0]",
|
||||
"src": "build_workflow.S3",
|
||||
"srcArrow": "triangle",
|
||||
"srcLabel": "",
|
||||
"dst": "build_workflow.Terraform",
|
||||
"dstArrow": "triangle",
|
||||
"dstLabel": "",
|
||||
"opacity": 1,
|
||||
"strokeDash": 0,
|
||||
"strokeWidth": 2,
|
||||
"stroke": "#0D32B2",
|
||||
"label": "Pulls zip to deploy",
|
||||
"fontSize": 16,
|
||||
"fontFamily": "DEFAULT",
|
||||
"language": "",
|
||||
"color": "#676C7E",
|
||||
"italic": true,
|
||||
"bold": false,
|
||||
"underline": false,
|
||||
"labelWidth": 119,
|
||||
"labelHeight": 21,
|
||||
"labelPosition": "INSIDE_MIDDLE_CENTER",
|
||||
"labelPercentage": 0,
|
||||
"route": [
|
||||
{
|
||||
"x": 1253.5,
|
||||
"y": 118.5
|
||||
},
|
||||
{
|
||||
"x": 1336.3,
|
||||
"y": 118.5
|
||||
},
|
||||
{
|
||||
"x": 1377.9,
|
||||
"y": 118.5
|
||||
},
|
||||
{
|
||||
"x": 1461.5,
|
||||
"y": 118.5
|
||||
}
|
||||
],
|
||||
"isCurve": true,
|
||||
"animated": false,
|
||||
"tooltip": "",
|
||||
"icon": null,
|
||||
"zIndex": 0
|
||||
},
|
||||
{
|
||||
"id": "build_workflow.(Terraform -> AWS)[0]",
|
||||
"src": "build_workflow.Terraform",
|
||||
"srcArrow": "none",
|
||||
"srcLabel": "",
|
||||
"dst": "build_workflow.AWS",
|
||||
"dstArrow": "triangle",
|
||||
"dstLabel": "",
|
||||
"opacity": 1,
|
||||
"strokeDash": 0,
|
||||
"strokeWidth": 2,
|
||||
"stroke": "#0D32B2",
|
||||
"label": "Changes the live lambdas",
|
||||
"fontSize": 16,
|
||||
"fontFamily": "DEFAULT",
|
||||
"language": "",
|
||||
"color": "#676C7E",
|
||||
"italic": true,
|
||||
"bold": false,
|
||||
"underline": false,
|
||||
"labelWidth": 169,
|
||||
"labelHeight": 21,
|
||||
"labelPosition": "INSIDE_MIDDLE_CENTER",
|
||||
"labelPercentage": 0,
|
||||
"route": [
|
||||
{
|
||||
"x": 1680.5,
|
||||
"y": 118.5
|
||||
},
|
||||
{
|
||||
"x": 1763.3,
|
||||
"y": 118.5
|
||||
},
|
||||
{
|
||||
"x": 1804.9,
|
||||
"y": 118.5
|
||||
},
|
||||
{
|
||||
"x": 1888.5,
|
||||
"y": 118.5
|
||||
}
|
||||
],
|
||||
"isCurve": true,
|
||||
"animated": false,
|
||||
"tooltip": "",
|
||||
"icon": null,
|
||||
"zIndex": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
48
e2etests/testdata/regression/dagre_edge_label_spacing/dagre/sketch.exp.svg
vendored
Normal file
|
After Width: | Height: | Size: 794 KiB |
404
e2etests/testdata/regression/dagre_edge_label_spacing/elk/board.exp.json
generated
vendored
Normal file
|
|
@ -0,0 +1,404 @@
|
|||
{
|
||||
"name": "",
|
||||
"fontFamily": "SourceSansPro",
|
||||
"shapes": [
|
||||
{
|
||||
"id": "build_workflow",
|
||||
"type": "",
|
||||
"pos": {
|
||||
"x": 12,
|
||||
"y": 12
|
||||
},
|
||||
"width": 1893,
|
||||
"height": 287,
|
||||
"opacity": 1,
|
||||
"strokeDash": 0,
|
||||
"strokeWidth": 2,
|
||||
"borderRadius": 0,
|
||||
"fill": "#E3E9FD",
|
||||
"stroke": "#0D32B2",
|
||||
"shadow": false,
|
||||
"3d": false,
|
||||
"multiple": false,
|
||||
"tooltip": "",
|
||||
"link": "",
|
||||
"icon": null,
|
||||
"iconPosition": "",
|
||||
"blend": false,
|
||||
"fields": null,
|
||||
"methods": null,
|
||||
"columns": null,
|
||||
"label": "lambda-build.yaml",
|
||||
"fontSize": 28,
|
||||
"fontFamily": "DEFAULT",
|
||||
"language": "",
|
||||
"color": "#0A0F25",
|
||||
"italic": false,
|
||||
"bold": false,
|
||||
"underline": false,
|
||||
"labelWidth": 226,
|
||||
"labelHeight": 41,
|
||||
"labelPosition": "INSIDE_TOP_CENTER",
|
||||
"zIndex": 0,
|
||||
"level": 1
|
||||
},
|
||||
{
|
||||
"id": "build_workflow.push",
|
||||
"type": "",
|
||||
"pos": {
|
||||
"x": 87,
|
||||
"y": 87
|
||||
},
|
||||
"width": 330,
|
||||
"height": 137,
|
||||
"opacity": 1,
|
||||
"strokeDash": 0,
|
||||
"strokeWidth": 2,
|
||||
"borderRadius": 0,
|
||||
"fill": "#EDF0FD",
|
||||
"stroke": "#0D32B2",
|
||||
"shadow": false,
|
||||
"3d": false,
|
||||
"multiple": false,
|
||||
"tooltip": "",
|
||||
"link": "",
|
||||
"icon": null,
|
||||
"iconPosition": "",
|
||||
"blend": false,
|
||||
"fields": null,
|
||||
"methods": null,
|
||||
"columns": null,
|
||||
"label": "Push to main branch",
|
||||
"fontSize": 25,
|
||||
"fontFamily": "DEFAULT",
|
||||
"language": "",
|
||||
"color": "#0A0F25",
|
||||
"italic": false,
|
||||
"bold": true,
|
||||
"underline": false,
|
||||
"labelWidth": 230,
|
||||
"labelHeight": 37,
|
||||
"labelPosition": "INSIDE_MIDDLE_CENTER",
|
||||
"zIndex": 0,
|
||||
"level": 2
|
||||
},
|
||||
{
|
||||
"id": "build_workflow.GHA",
|
||||
"type": "",
|
||||
"pos": {
|
||||
"x": 511,
|
||||
"y": 87
|
||||
},
|
||||
"width": 269,
|
||||
"height": 137,
|
||||
"opacity": 1,
|
||||
"strokeDash": 0,
|
||||
"strokeWidth": 2,
|
||||
"borderRadius": 0,
|
||||
"fill": "#EDF0FD",
|
||||
"stroke": "#0D32B2",
|
||||
"shadow": false,
|
||||
"3d": false,
|
||||
"multiple": false,
|
||||
"tooltip": "",
|
||||
"link": "",
|
||||
"icon": null,
|
||||
"iconPosition": "",
|
||||
"blend": false,
|
||||
"fields": null,
|
||||
"methods": null,
|
||||
"columns": null,
|
||||
"label": "GitHub Actions",
|
||||
"fontSize": 25,
|
||||
"fontFamily": "DEFAULT",
|
||||
"language": "",
|
||||
"color": "#0A0F25",
|
||||
"italic": false,
|
||||
"bold": true,
|
||||
"underline": false,
|
||||
"labelWidth": 169,
|
||||
"labelHeight": 37,
|
||||
"labelPosition": "INSIDE_MIDDLE_CENTER",
|
||||
"zIndex": 0,
|
||||
"level": 2
|
||||
},
|
||||
{
|
||||
"id": "build_workflow.S3",
|
||||
"type": "",
|
||||
"pos": {
|
||||
"x": 958,
|
||||
"y": 87
|
||||
},
|
||||
"width": 131,
|
||||
"height": 137,
|
||||
"opacity": 1,
|
||||
"strokeDash": 0,
|
||||
"strokeWidth": 2,
|
||||
"borderRadius": 0,
|
||||
"fill": "#EDF0FD",
|
||||
"stroke": "#0D32B2",
|
||||
"shadow": false,
|
||||
"3d": false,
|
||||
"multiple": false,
|
||||
"tooltip": "",
|
||||
"link": "",
|
||||
"icon": null,
|
||||
"iconPosition": "",
|
||||
"blend": false,
|
||||
"fields": null,
|
||||
"methods": null,
|
||||
"columns": null,
|
||||
"label": "S3",
|
||||
"fontSize": 25,
|
||||
"fontFamily": "DEFAULT",
|
||||
"language": "",
|
||||
"color": "#0A0F25",
|
||||
"italic": false,
|
||||
"bold": true,
|
||||
"underline": false,
|
||||
"labelWidth": 31,
|
||||
"labelHeight": 37,
|
||||
"labelPosition": "INSIDE_MIDDLE_CENTER",
|
||||
"zIndex": 0,
|
||||
"level": 2
|
||||
},
|
||||
{
|
||||
"id": "build_workflow.Terraform",
|
||||
"type": "",
|
||||
"pos": {
|
||||
"x": 1248,
|
||||
"y": 87
|
||||
},
|
||||
"width": 218,
|
||||
"height": 137,
|
||||
"opacity": 1,
|
||||
"strokeDash": 0,
|
||||
"strokeWidth": 2,
|
||||
"borderRadius": 0,
|
||||
"fill": "#EDF0FD",
|
||||
"stroke": "#0D32B2",
|
||||
"shadow": false,
|
||||
"3d": false,
|
||||
"multiple": false,
|
||||
"tooltip": "",
|
||||
"link": "",
|
||||
"icon": null,
|
||||
"iconPosition": "",
|
||||
"blend": false,
|
||||
"fields": null,
|
||||
"methods": null,
|
||||
"columns": null,
|
||||
"label": "Terraform",
|
||||
"fontSize": 25,
|
||||
"fontFamily": "DEFAULT",
|
||||
"language": "",
|
||||
"color": "#0A0F25",
|
||||
"italic": false,
|
||||
"bold": true,
|
||||
"underline": false,
|
||||
"labelWidth": 118,
|
||||
"labelHeight": 37,
|
||||
"labelPosition": "INSIDE_MIDDLE_CENTER",
|
||||
"zIndex": 0,
|
||||
"level": 2
|
||||
},
|
||||
{
|
||||
"id": "build_workflow.AWS",
|
||||
"type": "",
|
||||
"pos": {
|
||||
"x": 1675,
|
||||
"y": 87
|
||||
},
|
||||
"width": 155,
|
||||
"height": 137,
|
||||
"opacity": 1,
|
||||
"strokeDash": 0,
|
||||
"strokeWidth": 2,
|
||||
"borderRadius": 0,
|
||||
"fill": "#EDF0FD",
|
||||
"stroke": "#0D32B2",
|
||||
"shadow": false,
|
||||
"3d": false,
|
||||
"multiple": false,
|
||||
"tooltip": "",
|
||||
"link": "",
|
||||
"icon": null,
|
||||
"iconPosition": "",
|
||||
"blend": false,
|
||||
"fields": null,
|
||||
"methods": null,
|
||||
"columns": null,
|
||||
"label": "AWS",
|
||||
"fontSize": 25,
|
||||
"fontFamily": "DEFAULT",
|
||||
"language": "",
|
||||
"color": "#0A0F25",
|
||||
"italic": false,
|
||||
"bold": true,
|
||||
"underline": false,
|
||||
"labelWidth": 55,
|
||||
"labelHeight": 37,
|
||||
"labelPosition": "INSIDE_MIDDLE_CENTER",
|
||||
"zIndex": 0,
|
||||
"level": 2
|
||||
}
|
||||
],
|
||||
"connections": [
|
||||
{
|
||||
"id": "build_workflow.(push -> GHA)[0]",
|
||||
"src": "build_workflow.push",
|
||||
"srcArrow": "none",
|
||||
"srcLabel": "",
|
||||
"dst": "build_workflow.GHA",
|
||||
"dstArrow": "triangle",
|
||||
"dstLabel": "",
|
||||
"opacity": 1,
|
||||
"strokeDash": 0,
|
||||
"strokeWidth": 2,
|
||||
"stroke": "#0D32B2",
|
||||
"label": "Triggers",
|
||||
"fontSize": 16,
|
||||
"fontFamily": "DEFAULT",
|
||||
"language": "",
|
||||
"color": "#676C7E",
|
||||
"italic": true,
|
||||
"bold": false,
|
||||
"underline": false,
|
||||
"labelWidth": 54,
|
||||
"labelHeight": 21,
|
||||
"labelPosition": "INSIDE_MIDDLE_CENTER",
|
||||
"labelPercentage": 0,
|
||||
"route": [
|
||||
{
|
||||
"x": 417,
|
||||
"y": 155.5
|
||||
},
|
||||
{
|
||||
"x": 511,
|
||||
"y": 155.5
|
||||
}
|
||||
],
|
||||
"animated": false,
|
||||
"tooltip": "",
|
||||
"icon": null,
|
||||
"zIndex": 0
|
||||
},
|
||||
{
|
||||
"id": "build_workflow.(GHA -> S3)[0]",
|
||||
"src": "build_workflow.GHA",
|
||||
"srcArrow": "none",
|
||||
"srcLabel": "",
|
||||
"dst": "build_workflow.S3",
|
||||
"dstArrow": "triangle",
|
||||
"dstLabel": "",
|
||||
"opacity": 1,
|
||||
"strokeDash": 0,
|
||||
"strokeWidth": 2,
|
||||
"stroke": "#0D32B2",
|
||||
"label": "Builds zip & pushes it",
|
||||
"fontSize": 16,
|
||||
"fontFamily": "DEFAULT",
|
||||
"language": "",
|
||||
"color": "#676C7E",
|
||||
"italic": true,
|
||||
"bold": false,
|
||||
"underline": false,
|
||||
"labelWidth": 138,
|
||||
"labelHeight": 21,
|
||||
"labelPosition": "INSIDE_MIDDLE_CENTER",
|
||||
"labelPercentage": 0,
|
||||
"route": [
|
||||
{
|
||||
"x": 780,
|
||||
"y": 155.5
|
||||
},
|
||||
{
|
||||
"x": 958,
|
||||
"y": 155.5
|
||||
}
|
||||
],
|
||||
"animated": false,
|
||||
"tooltip": "",
|
||||
"icon": null,
|
||||
"zIndex": 0
|
||||
},
|
||||
{
|
||||
"id": "build_workflow.(S3 <-> Terraform)[0]",
|
||||
"src": "build_workflow.S3",
|
||||
"srcArrow": "triangle",
|
||||
"srcLabel": "",
|
||||
"dst": "build_workflow.Terraform",
|
||||
"dstArrow": "triangle",
|
||||
"dstLabel": "",
|
||||
"opacity": 1,
|
||||
"strokeDash": 0,
|
||||
"strokeWidth": 2,
|
||||
"stroke": "#0D32B2",
|
||||
"label": "Pulls zip to deploy",
|
||||
"fontSize": 16,
|
||||
"fontFamily": "DEFAULT",
|
||||
"language": "",
|
||||
"color": "#676C7E",
|
||||
"italic": true,
|
||||
"bold": false,
|
||||
"underline": false,
|
||||
"labelWidth": 119,
|
||||
"labelHeight": 21,
|
||||
"labelPosition": "INSIDE_MIDDLE_CENTER",
|
||||
"labelPercentage": 0,
|
||||
"route": [
|
||||
{
|
||||
"x": 1089,
|
||||
"y": 155.5
|
||||
},
|
||||
{
|
||||
"x": 1248,
|
||||
"y": 155.5
|
||||
}
|
||||
],
|
||||
"animated": false,
|
||||
"tooltip": "",
|
||||
"icon": null,
|
||||
"zIndex": 0
|
||||
},
|
||||
{
|
||||
"id": "build_workflow.(Terraform -> AWS)[0]",
|
||||
"src": "build_workflow.Terraform",
|
||||
"srcArrow": "none",
|
||||
"srcLabel": "",
|
||||
"dst": "build_workflow.AWS",
|
||||
"dstArrow": "triangle",
|
||||
"dstLabel": "",
|
||||
"opacity": 1,
|
||||
"strokeDash": 0,
|
||||
"strokeWidth": 2,
|
||||
"stroke": "#0D32B2",
|
||||
"label": "Changes the live lambdas",
|
||||
"fontSize": 16,
|
||||
"fontFamily": "DEFAULT",
|
||||
"language": "",
|
||||
"color": "#676C7E",
|
||||
"italic": true,
|
||||
"bold": false,
|
||||
"underline": false,
|
||||
"labelWidth": 169,
|
||||
"labelHeight": 21,
|
||||
"labelPosition": "INSIDE_MIDDLE_CENTER",
|
||||
"labelPercentage": 0,
|
||||
"route": [
|
||||
{
|
||||
"x": 1466,
|
||||
"y": 155.5
|
||||
},
|
||||
{
|
||||
"x": 1675,
|
||||
"y": 155.5
|
||||
}
|
||||
],
|
||||
"animated": false,
|
||||
"tooltip": "",
|
||||
"icon": null,
|
||||
"zIndex": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
48
e2etests/testdata/regression/dagre_edge_label_spacing/elk/sketch.exp.svg
vendored
Normal file
|
After Width: | Height: | Size: 794 KiB |
1
e2etests/testdata/regression/dagre_special_ids/dagre/board.exp.json
generated
vendored
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"name": "",
|
||||
"fontFamily": "SourceSansPro",
|
||||
"shapes": [
|
||||
{
|
||||
"id": "\"ninety\\nnine\"",
|
||||
|
|
|
|||
|
|
@ -18,7 +18,10 @@ width="1427" height="568" viewBox="-100 -100 1427 568"><style type="text/css">
|
|||
}
|
||||
|
||||
]]>
|
||||
</style><g id=""ninety\nnine""><g class="shape" ><rect x="0" y="0" width="151" height="142" style="fill:#F7F8FE;stroke:#0D32B2;opacity:1.000000;stroke-width:2;" /></g><text class="text-bold" x="75.500000" y="66.000000" style="text-anchor:middle;font-size:16px;fill:#0A0F25"><tspan x="75.500000" dy="0.000000">ninety</tspan><tspan x="75.500000" dy="21.000000">nine</tspan></text></g><g id="eighty
eight"><g class="shape" ><rect x="211" y="8" width="151" height="126" style="fill:#F7F8FE;stroke:#0D32B2;opacity:1.000000;stroke-width:2;" /></g><text class="text-bold" x="286.500000" y="74.000000" style="text-anchor:middle;font-size:16px;fill:#0A0F25">eighty
eight</text></g><g id=""seventy
\nseven""><g class="shape" ><rect x="422" y="0" width="162" height="142" style="fill:#F7F8FE;stroke:#0D32B2;opacity:1.000000;stroke-width:2;" /></g><text class="text-bold" x="503.000000" y="66.000000" style="text-anchor:middle;font-size:16px;fill:#0A0F25"><tspan x="503.000000" dy="0.000000">seventy
</tspan><tspan x="503.000000" dy="21.000000">seven</tspan></text></g><g id=""a\\yode""><g class="shape" ><rect x="644" y="8" width="154" height="126" style="fill:#F7F8FE;stroke:#0D32B2;opacity:1.000000;stroke-width:2;" /></g><text class="text-bold" x="721.000000" y="74.000000" style="text-anchor:middle;font-size:16px;fill:#0A0F25">a\yode</text></g><g id="there"><g class="shape" ><rect x="864" y="242" width="143" height="126" style="fill:#F7F8FE;stroke:#0D32B2;opacity:1.000000;stroke-width:2;" /></g><text class="text-bold" x="935.500000" y="308.000000" style="text-anchor:middle;font-size:16px;fill:#0A0F25">there</text></g><g id="'a\"ode'"><g class="shape" ><rect x="858" y="8" width="154" height="126" style="fill:#F7F8FE;stroke:#0D32B2;opacity:1.000000;stroke-width:2;" /></g><text class="text-bold" x="935.000000" y="74.000000" style="text-anchor:middle;font-size:16px;fill:#0A0F25">a\"ode</text></g><g id=""a\\node""><g class="shape" ><rect x="1072" y="8" width="155" height="126" style="fill:#F7F8FE;stroke:#0D32B2;opacity:1.000000;stroke-width:2;" /></g><text class="text-bold" x="1149.500000" y="74.000000" style="text-anchor:middle;font-size:16px;fill:#0A0F25">a\node</text></g><g id="("a\\yode" -> there)[0]"><marker id="mk-3990223579" markerWidth="10.000000" markerHeight="12.000000" refX="7.000000" refY="6.000000" viewBox="0.000000 0.000000 10.000000 12.000000" orient="auto" markerUnits="userSpaceOnUse"> <polygon class="connection" fill="#0D32B2" stroke-width="2" points="0.000000,0.000000 10.000000,6.000000 0.000000,12.000000" /> </marker><path d="M 721.000000 136.000000 C 721.000000 180.400000 749.500000 207.049065 859.962840 265.377574" class="connection" style="fill:none;stroke:#0D32B2;opacity:1.000000;stroke-width:2;" marker-end="url(#mk-3990223579)" mask="url(#4049001157)"/></g><g id="('a\"ode' -> there)[0]"><path d="M 935.000000 136.000000 C 935.000000 180.400000 935.000000 202.000000 935.000000 238.000000" class="connection" style="fill:none;stroke:#0D32B2;opacity:1.000000;stroke-width:2;" marker-end="url(#mk-3990223579)" mask="url(#4049001157)"/></g><g id="("a\\node" -> there)[0]"><path d="M 1149.500000 136.000000 C 1149.500000 180.400000 1120.900000 207.000000 1010.042356 265.142121" class="connection" style="fill:none;stroke:#0D32B2;opacity:1.000000;stroke-width:2;" marker-end="url(#mk-3990223579)" mask="url(#4049001157)"/></g><style type="text/css"><![CDATA[
|
||||
</style><g id=""ninety\nnine""><g class="shape" ><rect x="0" y="0" width="151" height="142" style="fill:#F7F8FE;stroke:#0D32B2;opacity:1.000000;stroke-width:2;" /></g><text class="text-bold" x="75.500000" y="66.000000" style="text-anchor:middle;font-size:16px;fill:#0A0F25"><tspan x="75.500000" dy="0.000000">ninety</tspan><tspan x="75.500000" dy="21.000000">nine</tspan></text></g><g id="eighty
eight"><g class="shape" ><rect x="211" y="8" width="151" height="126" style="fill:#F7F8FE;stroke:#0D32B2;opacity:1.000000;stroke-width:2;" /></g><text class="text-bold" x="286.500000" y="74.000000" style="text-anchor:middle;font-size:16px;fill:#0A0F25">eighty
eight</text></g><g id=""seventy
\nseven""><g class="shape" ><rect x="422" y="0" width="162" height="142" style="fill:#F7F8FE;stroke:#0D32B2;opacity:1.000000;stroke-width:2;" /></g><text class="text-bold" x="503.000000" y="66.000000" style="text-anchor:middle;font-size:16px;fill:#0A0F25"><tspan x="503.000000" dy="0.000000">seventy
</tspan><tspan x="503.000000" dy="21.000000">seven</tspan></text></g><g id=""a\\yode""><g class="shape" ><rect x="644" y="8" width="154" height="126" style="fill:#F7F8FE;stroke:#0D32B2;opacity:1.000000;stroke-width:2;" /></g><text class="text-bold" x="721.000000" y="74.000000" style="text-anchor:middle;font-size:16px;fill:#0A0F25">a\yode</text></g><g id="there"><g class="shape" ><rect x="864" y="242" width="143" height="126" style="fill:#F7F8FE;stroke:#0D32B2;opacity:1.000000;stroke-width:2;" /></g><text class="text-bold" x="935.500000" y="308.000000" style="text-anchor:middle;font-size:16px;fill:#0A0F25">there</text></g><g id="'a\"ode'"><g class="shape" ><rect x="858" y="8" width="154" height="126" style="fill:#F7F8FE;stroke:#0D32B2;opacity:1.000000;stroke-width:2;" /></g><text class="text-bold" x="935.000000" y="74.000000" style="text-anchor:middle;font-size:16px;fill:#0A0F25">a\"ode</text></g><g id=""a\\node""><g class="shape" ><rect x="1072" y="8" width="155" height="126" style="fill:#F7F8FE;stroke:#0D32B2;opacity:1.000000;stroke-width:2;" /></g><text class="text-bold" x="1149.500000" y="74.000000" style="text-anchor:middle;font-size:16px;fill:#0A0F25">a\node</text></g><g id="("a\\yode" -> there)[0]"><marker id="mk-3990223579" markerWidth="10.000000" markerHeight="12.000000" refX="7.000000" refY="6.000000" viewBox="0.000000 0.000000 10.000000 12.000000" orient="auto" markerUnits="userSpaceOnUse"> <polygon class="connection" fill="#0D32B2" stroke-width="2" points="0.000000,0.000000 10.000000,6.000000 0.000000,12.000000" /> </marker><path d="M 721.000000 136.000000 C 721.000000 180.400000 749.500000 207.049065 859.962840 265.377574" class="connection" style="fill:none;stroke:#0D32B2;opacity:1.000000;stroke-width:2;" marker-end="url(#mk-3990223579)" mask="url(#4049001157)"/></g><g id="('a\"ode' -> there)[0]"><path d="M 935.000000 136.000000 C 935.000000 180.400000 935.000000 202.000000 935.000000 238.000000" class="connection" style="fill:none;stroke:#0D32B2;opacity:1.000000;stroke-width:2;" marker-end="url(#mk-3990223579)" mask="url(#4049001157)"/></g><g id="("a\\node" -> there)[0]"><path d="M 1149.500000 136.000000 C 1149.500000 180.400000 1120.900000 207.000000 1010.042356 265.142121" class="connection" style="fill:none;stroke:#0D32B2;opacity:1.000000;stroke-width:2;" marker-end="url(#mk-3990223579)" mask="url(#4049001157)"/></g><mask id="4049001157" maskUnits="userSpaceOnUse" x="-100" y="-100" width="1427" height="568">
|
||||
<rect x="-100" y="-100" width="1427" height="568" fill="white"></rect>
|
||||
|
||||
</mask><style type="text/css"><![CDATA[
|
||||
.text-bold {
|
||||
font-family: "font-bold";
|
||||
}
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 327 KiB After Width: | Height: | Size: 327 KiB |