Merge branch 'master' into master
This commit is contained in:
commit
8762d9d784
1649 changed files with 341027 additions and 110338 deletions
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
|
|
@ -17,7 +17,7 @@ jobs:
|
|||
env:
|
||||
GITHUB_TOKEN: ${{ secrets._GITHUB_TOKEN }}
|
||||
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: d2chaos
|
||||
|
|
|
|||
|
|
@ -5,3 +5,4 @@ d2renderers/d2latex/polyfills.js
|
|||
d2renderers/d2latex/setup.js
|
||||
d2renderers/d2sketch/rough.js
|
||||
lib/png/generate_png.js
|
||||
d2js
|
||||
|
|
|
|||
5
Makefile
5
Makefile
|
|
@ -1,7 +1,7 @@
|
|||
.POSIX:
|
||||
|
||||
.PHONY: all
|
||||
all: fmt gen lint build test
|
||||
all: fmt gen js lint build test
|
||||
|
||||
.PHONY: fmt
|
||||
fmt:
|
||||
|
|
@ -21,3 +21,6 @@ test: fmt
|
|||
.PHONY: race
|
||||
race: fmt
|
||||
prefix "$@" ./ci/test.sh --race ./...
|
||||
.PHONY: js
|
||||
js: gen
|
||||
cd d2js/js && prefix "$@" ./make.sh all
|
||||
|
|
|
|||
|
|
@ -238,7 +238,7 @@ let us know and we'll be happy to include it here!
|
|||
|
||||
### Community plugins
|
||||
|
||||
- **Tree-sitter grammar**: [https://git.pleshevski.ru/pleshevskiy/tree-sitter-d2](https://git.pleshevski.ru/pleshevskiy/tree-sitter-d2)
|
||||
- **Tree-sitter grammar**: [https://github.com/ravsii/tree-sitter-d2](https://github.com/ravsii/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)
|
||||
|
|
@ -262,6 +262,7 @@ let us know and we'll be happy to include it here!
|
|||
- **MkDocs Plugin**: [https://github.com/landmaj/mkdocs-d2-plugin](https://github.com/landmaj/mkdocs-d2-plugin)
|
||||
- **Remark Plugin**: [https://github.com/mech-a/remark-d2](https://github.com/mech-a/remark-d2)
|
||||
- **VitePress Plugin**: [https://github.com/BadgerHobbs/vitepress-plugin-d2](https://github.com/BadgerHobbs/vitepress-plugin-d2)
|
||||
- **Zed extension**: [https://github.com/gabeidx/zed-d2](https://github.com/gabeidx/zed-d2)
|
||||
|
||||
### Misc
|
||||
|
||||
|
|
@ -292,6 +293,12 @@ Do you have or see an open-source project with `.d2` files? Please submit a PR a
|
|||
this selected list of featured projects using D2.
|
||||
|
||||
- [ElasticSearch](https://github.com/elastic/beats/blob/main/libbeat/publisher/queue/proxy/diagrams/broker.d2)
|
||||
- [Sourcegraph](https://handbook.sourcegraph.com/departments/engineering/managed-services/telemetry-gateway/#dev-architecture-diagram)
|
||||
- [Temporal](https://github.com/temporalio/temporal/blob/0be2681c994470c7c61ea88e4fcef89bb4024e58/docs/_assets/matching-context.d2)
|
||||
- [Tauri](https://v2.tauri.app/concept/inter-process-communication/)
|
||||
- Rust GUI framework (78.5k stars)
|
||||
- [Intellij](https://github.com/JetBrains/intellij-community/blob/45bcfc17a3f3e0d8548bc69e922d4ca97ac21b2b/platform/settings/docs/topics/overview.md)
|
||||
- [Coder](https://coder.com/blog/managing-templates-in-coder)
|
||||
- [UC
|
||||
Berkeley](https://github.com/ucb-bar/hammer/blob/2b5c04d7b7d9ee3c73575efcd7ee0698bd5bfa88/doc/Hammer-Use/hier.d2)
|
||||
- [Coronacheck](https://github.com/minvws/nl-covid19-coronacheck-app-ios/blob/e1567e9d1633b3273c537a105bff0e7d3a57ecfe/Diagrams/client-side-datamodel.d2)
|
||||
|
|
|
|||
28
ci/peek-wasm-size.sh
Executable file
28
ci/peek-wasm-size.sh
Executable file
|
|
@ -0,0 +1,28 @@
|
|||
#!/bin/bash
|
||||
|
||||
OUTPUT_FILE="main.wasm"
|
||||
SOURCE_PACKAGE="./d2js"
|
||||
|
||||
echo "Building WASM file..."
|
||||
GOOS=js GOARCH=wasm go build -ldflags='-s -w' -trimpath -o "$OUTPUT_FILE" "$SOURCE_PACKAGE"
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "Build successful."
|
||||
|
||||
if [ -f "$OUTPUT_FILE" ]; then
|
||||
FILE_SIZE_BYTES=$(stat -f%z "$OUTPUT_FILE")
|
||||
FILE_SIZE_MB=$(echo "scale=2; $FILE_SIZE_BYTES / 1024 / 1024" | bc)
|
||||
|
||||
echo "File size of $OUTPUT_FILE: $FILE_SIZE_MB MB"
|
||||
else
|
||||
echo "File $OUTPUT_FILE not found!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Deleting $OUTPUT_FILE..."
|
||||
rm "$OUTPUT_FILE"
|
||||
echo "File deleted."
|
||||
else
|
||||
echo "Build failed."
|
||||
exit 1
|
||||
fi
|
||||
|
|
@ -10,7 +10,7 @@ cd -- "$(dirname "$0")/../.."
|
|||
|
||||
help() {
|
||||
cat <<EOF
|
||||
usage: $0 [--rebuild] [--local] [--dry-run] [--run=regex] [--host-only] [--lockfile-force]
|
||||
usage: $0 [--rebuild] [--dry-run] [--run=regex] [--host-only] [--lockfile-force]
|
||||
[--install] [--uninstall]
|
||||
|
||||
$0 builds D2 release archives into ./ci/release/build/<version>/d2-<VERSION>-<OS>-<ARCH>.tar.gz
|
||||
|
|
@ -24,14 +24,6 @@ Flags:
|
|||
By default build.sh will avoid rebuilding finished assets if they already exist but if you
|
||||
changed something and need to force rebuild, use this flag.
|
||||
|
||||
--local
|
||||
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!
|
||||
|
||||
--host-only
|
||||
Use to build the release archive for the host OS-ARCH only. All logging is done to stderr
|
||||
so in a script you can read from stdout to get the path to the release archive.
|
||||
|
|
@ -75,10 +67,6 @@ main() {
|
|||
flag_noarg && shift "$FLAGSHIFT"
|
||||
REBUILD=1
|
||||
;;
|
||||
local)
|
||||
flag_noarg && shift "$FLAGSHIFT"
|
||||
LOCAL=1
|
||||
;;
|
||||
dry-run)
|
||||
flag_noarg && shift "$FLAGSHIFT"
|
||||
DRY_RUN=1
|
||||
|
|
@ -90,7 +78,6 @@ main() {
|
|||
host-only)
|
||||
flag_noarg && shift "$FLAGSHIFT"
|
||||
HOST_ONLY=1
|
||||
LOCAL=1
|
||||
;;
|
||||
version)
|
||||
flag_nonemptyarg && shift "$FLAGSHIFT"
|
||||
|
|
@ -104,13 +91,11 @@ main() {
|
|||
flag_noarg && shift "$FLAGSHIFT"
|
||||
INSTALL=1
|
||||
HOST_ONLY=1
|
||||
LOCAL=1
|
||||
;;
|
||||
uninstall)
|
||||
flag_noarg && shift "$FLAGSHIFT"
|
||||
UNINSTALL=1
|
||||
HOST_ONLY=1
|
||||
LOCAL=1
|
||||
;;
|
||||
push-docker)
|
||||
flag_noarg && shift "$FLAGSHIFT"
|
||||
|
|
@ -170,45 +155,8 @@ build() {
|
|||
return 0
|
||||
fi
|
||||
|
||||
if [ -n "${LOCAL-}" ]; then
|
||||
build_local
|
||||
return 0
|
||||
fi
|
||||
|
||||
case $OS in
|
||||
macos)
|
||||
case $ARCH in
|
||||
amd64)
|
||||
REMOTE_HOST=$CI_D2_MACOS_AMD64 build_remote_macos
|
||||
;;
|
||||
arm64)
|
||||
REMOTE_HOST=$CI_D2_MACOS_ARM64 build_remote_macos
|
||||
;;
|
||||
*)
|
||||
warn "no builder for OS=$OS ARCH=$ARCH, building locally..."
|
||||
build_local
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
linux)
|
||||
case $ARCH in
|
||||
amd64)
|
||||
REMOTE_HOST=$CI_D2_LINUX_AMD64 build_remote_linux
|
||||
;;
|
||||
arm64)
|
||||
REMOTE_HOST=$CI_D2_LINUX_ARM64 build_remote_linux
|
||||
;;
|
||||
*)
|
||||
warn "no builder for OS=$OS ARCH=$ARCH, building locally..."
|
||||
build_local
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
*)
|
||||
warn "no builder for OS=$OS, building locally..."
|
||||
build_local
|
||||
;;
|
||||
esac
|
||||
build_local
|
||||
return 0
|
||||
}
|
||||
|
||||
build_local() {
|
||||
|
|
@ -221,39 +169,6 @@ build_local() {
|
|||
sh_c ./ci/release/_build.sh
|
||||
}
|
||||
|
||||
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-} \
|
||||
TERM=${TERM-} \
|
||||
DRY_RUN=${DRY_RUN-} \
|
||||
HW_BUILD_DIR=$HW_BUILD_DIR \
|
||||
VERSION=$VERSION \
|
||||
OS=$OS \
|
||||
ARCH=$ARCH \
|
||||
ARCHIVE=$ARCHIVE \
|
||||
PATH=\\\"/usr/local/bin:/usr/local/sbin:/opt/homebrew/bin:/opt/homebrew/sbin\\\${PATH+:\\\$PATH}\\\" \
|
||||
./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() {(
|
||||
sh_c lockfile_ssh "$REMOTE_HOST" .d2-build-lock
|
||||
sh_c gitsync "$REMOTE_HOST" src/d2
|
||||
sh_c ssh "$REMOTE_HOST" "COLOR=${COLOR-} \
|
||||
TERM=${TERM-} \
|
||||
DRY_RUN=${DRY_RUN-} \
|
||||
HW_BUILD_DIR=$HW_BUILD_DIR \
|
||||
VERSION=$VERSION \
|
||||
OS=$OS \
|
||||
ARCH=$ARCH \
|
||||
ARCHIVE=$ARCHIVE \
|
||||
./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() {
|
||||
if [ -n "${LOCAL-}" ]; then
|
||||
sh_c ./ci/release/docker/build.sh \
|
||||
|
|
|
|||
|
|
@ -1,3 +1,35 @@
|
|||
Hotfix for 0.6.4 breaking plugins, along with 2 other compiler bugfixes.
|
||||
#### Features 🚀
|
||||
|
||||
- Animations: `style.animated: true` is supported on shapes [#2250](https://github.com/terrastruct/d2/pull/2250)
|
||||
- Connections now support `link` [#1955](https://github.com/terrastruct/d2/pull/1955)
|
||||
- Vars: vars in markdown blocks are substituted [#2218](https://github.com/terrastruct/d2/pull/2218)
|
||||
- Markdown: Github-flavored tables work in `md` blocks [#2221](https://github.com/terrastruct/d2/pull/2221)
|
||||
- Render: adds box arrowheads [#2227](https://github.com/terrastruct/d2/issues/2227)
|
||||
- `d2 fmt` now supports a `--check` flag [#2253](https://github.com/terrastruct/d2/pull/2253)
|
||||
- CLI: PNG output to stdout is supported using `--stdout-format png -` [#2291](https://github.com/terrastruct/d2/pull/2291)
|
||||
- Globs: `&connected` and `&leaf` filters are implemented [#2299](https://github.com/terrastruct/d2/pull/2299)
|
||||
- CLI: add --no-xml-tag for direct HTML embedding [#2302](https://github.com/terrastruct/d2/pull/2302)
|
||||
- CLI: `play` cmd added for opening d2 input in online playground [#2242](https://github.com/terrastruct/d2/pull/2242)
|
||||
|
||||
#### Improvements 🧹
|
||||
|
||||
- Composition: links pointing to own board are purged [#2203](https://github.com/terrastruct/d2/pull/2203)
|
||||
- Syntax: reserved keywords must be unquoted [#2231](https://github.com/terrastruct/d2/pull/2231)
|
||||
- Latex: Backslashes in Latex blocks do not escape [#2232](https://github.com/terrastruct/d2/pull/2232)
|
||||
- This is a breaking change. Previously Latex blocks required escaping the backslash. So
|
||||
for older D2 versions, you should remove the excess backslashes.
|
||||
- Links: non-http url scheme links are supported (e.g. `x.link: vscode://file/`) [#2237](https://github.com/terrastruct/d2/issues/2237)
|
||||
- Compiler: reserved keywords with missing values error instead of silently doing nothing [#2251](https://github.com/terrastruct/d2/pull/2251)
|
||||
- Render: SVG outputs conform to stricter HTML standards, e.g. no duplicate ids [#2273](https://github.com/terrastruct/d2/issues/2273)
|
||||
- Themes: theme names are consistently cased [#2322](https://github.com/terrastruct/d2/pull/2322)
|
||||
- Nears: constant nears avoid collision with edge routes [#2327](https://github.com/terrastruct/d2/pull/2327)
|
||||
|
||||
#### Bugfixes ⛑️
|
||||
|
||||
- Imports: fixes using substitutions in `icon` values [#2207](https://github.com/terrastruct/d2/pull/2207)
|
||||
- Markdown: fixes ampersands in URLs in markdown [#2219](https://github.com/terrastruct/d2/pull/2219)
|
||||
- Globs: fixes edge case where globs with imported boards would create empty boards [#2247](https://github.com/terrastruct/d2/pull/2247)
|
||||
- Sequence diagrams: fixes alignment of notes when self messages are above it [#2264](https://github.com/terrastruct/d2/pull/2264)
|
||||
- Null: fixes `null`ing a connection with absolute syntax [#2318](https://github.com/terrastruct/d2/issues/2318)
|
||||
- Gradients: works with connection fills [#2326](https://github.com/terrastruct/d2/pull/2326)
|
||||
- Latex: fixes backslashes doubling on successive parses [#2328](https://github.com/terrastruct/d2/pull/2328)
|
||||
|
|
|
|||
23
ci/release/changelogs/v0.6.6.md
Normal file
23
ci/release/changelogs/v0.6.6.md
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
#### Features 🚀
|
||||
|
||||
- Glob inverse filters are implemented (e.g. `*: {!&shape: circle; style.fill: red}` to turn all non-circles red) [#2008](https://github.com/terrastruct/d2/pull/2008)
|
||||
- Globs can be used in glob filter values, including checking for existence (e.g. `*: {&link: *; style.fill: red}` to turn all objects with a link red) [#2009](https://github.com/terrastruct/d2/pull/2009)
|
||||
|
||||
#### Improvements 🧹
|
||||
|
||||
- Opacity 0 shapes no longer have a label mask which made any segment of connections going through them lower opacity [#1940](https://github.com/terrastruct/d2/pull/1940)
|
||||
- Bidirectional connections are now animated in opposite directions rather than one direction [#1939](https://github.com/terrastruct/d2/pull/1939)
|
||||
|
||||
#### Bugfixes ⛑️
|
||||
|
||||
- Local relative icons are relative to the d2 file instead of CLI invoke path [#1924](https://github.com/terrastruct/d2/pull/1924)
|
||||
- Custom label positions weren't being read when the width was smaller than the label [#1928](https://github.com/terrastruct/d2/pull/1928)
|
||||
- Using `shape: circle` for arrowheads no longer removes all arrowheads along path in sketch mode [#1942](https://github.com/terrastruct/d2/pull/1942)
|
||||
- Globs to null connections work [#1965](https://github.com/terrastruct/d2/pull/1965)
|
||||
- Edge globs setting styles inherit correctly in child boards [#1967](https://github.com/terrastruct/d2/pull/1967)
|
||||
- Board links imported with spread imports work [#1972](https://github.com/terrastruct/d2/pull/1972)
|
||||
- Fix importing a file with nested boards [#1998](https://github.com/terrastruct/d2/pull/1998)
|
||||
- Fix importing a file with underscores in links [#1999](https://github.com/terrastruct/d2/pull/1999)
|
||||
- Replace a panic with an error message resulting from invalid `link` usage [#2011](https://github.com/terrastruct/d2/pull/2011)
|
||||
- Fix globs not applying to scenarios on keys that were applied in earlier scenarios [#2021](https://github.com/terrastruct/d2/pull/2021)
|
||||
- Fix edge case of invalid SVG from code blocks [#2031](https://github.com/terrastruct/d2/pull/2031)
|
||||
38
ci/release/changelogs/v0.6.7.md
Normal file
38
ci/release/changelogs/v0.6.7.md
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
#### Features 🚀
|
||||
|
||||
- Vars: Variable definitions can refer to other variables in the current scope [#2052](https://github.com/terrastruct/d2/pull/2052)
|
||||
- Composition: Imported boards can use underscores to reference boards beyond its own scope (e.g. to a sibling board at the scope its imported to) [#2075](https://github.com/terrastruct/d2/pull/2075)
|
||||
- Autoformat: Reserved keywords are formatted to be lowercase [#2098](https://github.com/terrastruct/d2/pull/2098)
|
||||
- Misc: support for characters in the Latin-1 and geometric shapes unicode range [#2100](https://github.com/terrastruct/d2/pull/2100)
|
||||
- Imports: can now import from absolute file paths [#2113](https://github.com/terrastruct/d2/pull/2113)
|
||||
- Render: linear and radial gradients are now available for `fill`, `stroke` and `font-color` [#2120](https://github.com/terrastruct/d2/pull/2120)
|
||||
|
||||
#### Improvements 🧹
|
||||
|
||||
- Sequence diagram: edge groups account for edge label heights [#2038](https://github.com/terrastruct/d2/pull/2038)
|
||||
- Sequence diagram: self-referential edges account for edge label heights [#2040](https://github.com/terrastruct/d2/pull/2040)
|
||||
- Sequence diagram: The spacing between self-referential edges and regular edges is uniform [#2043](https://github.com/terrastruct/d2/pull/2043)
|
||||
- Compiler: Error on multi-line labels in `sql_table` shapes [#2057](https://github.com/terrastruct/d2/pull/2057)
|
||||
- Sequence diagram: Image shape actors can use spans and notes [#2056](https://github.com/terrastruct/d2/issues/2056)
|
||||
- Globs: Filters work with default values (e.g. `&opacity: 1` will capture everything without opacity explicitly set) [#2090](https://github.com/terrastruct/d2/pull/2090)
|
||||
- Render: connection label fills have a bit of padding and border-radius for better aesthetics [#2094](https://github.com/terrastruct/d2/pull/2094)
|
||||
- Sequence diagram: the padding between message labels and message endpoints are slightly increased [#2096](https://github.com/terrastruct/d2/pull/2096)
|
||||
- Render: code syntax highlighter dependency upgrade caused some slight subtle color changes in code snippets [#2119](https://github.com/terrastruct/d2/pull/2119)
|
||||
|
||||
|
||||
#### Bugfixes ⛑️
|
||||
|
||||
- Sequence diagram: multi-line edge labels no longer can collide with other elements [#2049](https://github.com/terrastruct/d2/pull/2049)
|
||||
- Sequence diagram: long self-referential edge labels no longer can collide neighboring actors (or its own) lifeline edges [#2050](https://github.com/terrastruct/d2/pull/2050)
|
||||
- Sequence diagram: fixes layout when sequence diagrams are in children boards (e.g. a layer) [#1692](https://github.com/terrastruct/d2/issues/1692)
|
||||
- Globs: An edge case was fixed where globs used in edges were creating nodes when it shouldn't have [#2051](https://github.com/terrastruct/d2/pull/2051)
|
||||
- Render: Multi-line class labels/headers are rendered correctly [#2057](https://github.com/terrastruct/d2/pull/2057)
|
||||
- CLI: Watch mode uses correct backlinks (`_` usages) [#2058](https://github.com/terrastruct/d2/pull/2058)
|
||||
- Vars: Spread variables are inserted in place instead of appending to end of scope [#2062](https://github.com/terrastruct/d2/pull/2062)
|
||||
- Imports: fix local icon imports from files that are imported [#2066](https://github.com/terrastruct/d2/pull/2066)
|
||||
- CLI: fixes edge case of watch mode links to nested board that had more nested boards not working [#2070](https://github.com/terrastruct/d2/pull/2070)
|
||||
- CLI: fixes theme flag not being passed to GIF outputs [#2071](https://github.com/terrastruct/d2/pull/2071)
|
||||
- CLI: fixes scale flag not being passed to animated SVG outputs [#2071](https://github.com/terrastruct/d2/pull/2071)
|
||||
- CLI: pptx exports use theme flags correctly [#2099](https://github.com/terrastruct/d2/pull/2099)
|
||||
- Imports: importing files with url links is fixed [#2105](https://github.com/terrastruct/d2/pull/2105)
|
||||
- Composition: linking to invalid boards no longer produces an invalid link [#2118](https://github.com/terrastruct/d2/pull/2118)
|
||||
17
ci/release/changelogs/v0.6.8.md
Normal file
17
ci/release/changelogs/v0.6.8.md
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
#### Features 🚀
|
||||
|
||||
- Render: SVG files render in non-browser contexts (e.g. Inkscape, LaTeX) [#2147](https://github.com/terrastruct/d2/pull/2147)
|
||||
|
||||
#### Improvements 🧹
|
||||
|
||||
- Lib: removes a dependency on external slog that was causing troubles with installation [#2137](https://github.com/terrastruct/d2/pull/2137)
|
||||
- CLI: attempts writing to path atomically, falling back to non-atomic if failed [#2141](https://github.com/terrastruct/d2/pull/2141)
|
||||
- Export: pptx has "created at" metadata removed, so successive runs yield the same result [#2169](https://github.com/terrastruct/d2/pull/2160)
|
||||
- Formatter: empty board keywords (e.g. `layers`) are removed [#2178](https://github.com/terrastruct/d2/pull/2178)
|
||||
- Render: a tooltip or link by itself will not expand width of shape [#2183](https://github.com/terrastruct/d2/pull/2183)
|
||||
|
||||
#### Bugfixes ⛑️
|
||||
|
||||
- Render: fixes edge case of a 3d shape with outside label being cut off [#2132](https://github.com/terrastruct/d2/pull/2132)
|
||||
- Composition: labels for boards set with shorthand `x: y` was not applied [#2182](https://github.com/terrastruct/d2/pull/2182)
|
||||
- Globs: double globs (`**`) were erroring when used with multiple scenario boards [#2195](https://github.com/terrastruct/d2/pull/2195)
|
||||
|
|
@ -5,9 +5,9 @@ ARG TARGETARCH
|
|||
|
||||
RUN apt-get update && apt-get install -y ca-certificates curl dumb-init sudo
|
||||
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_19.x | bash -s - && \
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash -s - && \
|
||||
apt-get install -y nodejs
|
||||
RUN npx playwright@1.31.1 install --with-deps chromium
|
||||
RUN npx playwright@1.45.3 install --with-deps chromium
|
||||
|
||||
RUN adduser --gecos '' --disabled-password debian \
|
||||
&& echo "debian ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers.d/nopasswd
|
||||
|
|
|
|||
|
|
@ -8,12 +8,15 @@
|
|||
.Nm d2
|
||||
.Op Fl -watch Ar false
|
||||
.Op Fl -theme Em 0
|
||||
.Op Fl -salt Ar string
|
||||
.Ar file.d2
|
||||
.Op Ar file.svg | file.png
|
||||
.Nm d2
|
||||
.Ar layout Op Ar name
|
||||
.Nm d2
|
||||
.Ar fmt Ar file.d2 ...
|
||||
.Nm d2
|
||||
.Ar play Ar file.d2
|
||||
.Sh DESCRIPTION
|
||||
.Nm
|
||||
compiles and renders
|
||||
|
|
@ -96,7 +99,7 @@ Path to .ttf file to use for the bold font. If none provided, Source Sans Pro Bo
|
|||
Pixels padded around the rendered diagram
|
||||
.Ns .
|
||||
.It Fl -animate-interval Ar 0
|
||||
If given, multiple boards are packaged as 1 SVG which transitions through each board at the interval (in milliseconds). Can only be used with SVG exports
|
||||
If given, multiple boards are packaged as 1 SVG which transitions through each board at the interval (in milliseconds). Can only be used with SVG and GIF exports
|
||||
.Ns .
|
||||
.It Fl -browser Ar true
|
||||
Browser executable that watch opens. Setting to 0 opens no browser
|
||||
|
|
@ -125,12 +128,24 @@ In watch mode, images used in icons are cached for subsequent compilations. This
|
|||
.It Fl -timeout Ar 120
|
||||
The maximum number of seconds that D2 runs for before timing out and exiting. When rendering a large diagram, it is recommended to increase this value
|
||||
.Ns .
|
||||
.It Fl -check Ar false
|
||||
Check that the specified files are formatted correctly
|
||||
.Ns .
|
||||
.It Fl -salt Ar string
|
||||
Add a salt value to ensure the output uses unique IDs. This is useful when generating multiple identical diagrams to be included in the same HTML doc, so that duplicate id's do not cause invalid HTML. The salt value is a string that will be appended to IDs in the output.
|
||||
.Ns .
|
||||
.It Fl h , -help
|
||||
Print usage information and exit
|
||||
.Ns .
|
||||
.It Fl v , -version
|
||||
Print version information and exit
|
||||
.Ns .
|
||||
.It Fl -stdout-format Ar string
|
||||
Set the output format when writing to stdout. Supported formats are: png, svg. Only used when output is set to stdout (-)
|
||||
.Ns .
|
||||
.It Fl -no-xml-tag Ar false
|
||||
Omit XML tag (<?xml ...?>) from output SVG files. Useful when generating SVGs for direct HTML embedding
|
||||
.Ns .
|
||||
.El
|
||||
.Sh SUBCOMMANDS
|
||||
.Bl -tag -width Fl
|
||||
|
|
@ -145,7 +160,60 @@ Lists available themes
|
|||
.Ns .
|
||||
.It Ar fmt Ar file.d2 ...
|
||||
Format all passed files
|
||||
.Ns .
|
||||
.It Ar play Ar file.d2
|
||||
Opens the file in playground, an online web viewer (https://play.d2lang.com)
|
||||
.El
|
||||
.Sh ENVIRONMENT VARIABLES
|
||||
Many flags can also be set with environment variables.
|
||||
.Bl -tag -width Ds
|
||||
.It Ev Sy D2_WATCH
|
||||
See -w[atch] flag.
|
||||
.It Ev Sy D2_LAYOUT
|
||||
See -l[ayout] flag.
|
||||
.It Ev Sy D2_THEME
|
||||
See -t[heme] flag.
|
||||
.It Ev Sy D2_DARK_THEME
|
||||
See --dark-theme flag.
|
||||
.It Ev Sy D2_PAD
|
||||
See --pad flag.
|
||||
.It Ev Sy D2_CENTER
|
||||
See --center flag.
|
||||
.It Ev Sy D2_SKETCH
|
||||
See -s[ketch] flag.
|
||||
.It Ev Sy D2_BUNDLE
|
||||
See -b[undle] flag.
|
||||
.It Ev Sy D2_FORCE_APPENDIX
|
||||
See --force-appendix flag.
|
||||
.It Ev Sy D2_FONT_REGULAR
|
||||
See --font-regular flag.
|
||||
.It Ev Sy D2_FONT_ITALIC
|
||||
See --font-italic flag.
|
||||
.It Ev Sy D2_FONT_BOLD
|
||||
See --font-bold flag.
|
||||
.It Ev Sy D2_FONT_SEMIBOLD
|
||||
See --font-semibold flag.
|
||||
.It Ev Sy D2_ANIMATE_INTERVAL
|
||||
See --animate-interval flag.
|
||||
.It Ev Sy D2_TIMEOUT
|
||||
See --timeout flag.
|
||||
.It Ev Sy D2_CHECK
|
||||
See --check flag.
|
||||
.El
|
||||
.Bl -tag -width Ds
|
||||
.It Ev Sy DEBUG
|
||||
See -d[ebug] flag.
|
||||
.It Ev Sy IMG_CACHE
|
||||
See --img-cache flag.
|
||||
.It Ev Sy HOST
|
||||
See -h[ost] flag.
|
||||
.It Ev Sy PORT
|
||||
See -p[ort] flag.
|
||||
.It Ev Sy BROWSER
|
||||
See --browser flag.
|
||||
.It Ev Sy D2_STDOUT_FORMAT
|
||||
See --stdout-format flag.
|
||||
.It Ev Sy D2_NO_XML_TAG
|
||||
See --no-xml-tag flag.
|
||||
.El
|
||||
.Sh SEE ALSO
|
||||
.Xr d2plugin-tala 1
|
||||
|
|
|
|||
2
ci/sub
2
ci/sub
|
|
@ -1 +1 @@
|
|||
Subproject commit 7a2914b504ed0dfca6d2dcd923b660052217cccb
|
||||
Subproject commit 2594100ac939644f134e24edac7c9bcd569b99f6
|
||||
200
d2ast/d2ast.go
200
d2ast/d2ast.go
|
|
@ -49,8 +49,7 @@ type Node interface {
|
|||
// GetRange returns the range a node occupies in its file.
|
||||
GetRange() Range
|
||||
|
||||
// TODO: add Children() for walking AST
|
||||
// Children() []Node
|
||||
Children() []Node
|
||||
}
|
||||
|
||||
var _ Node = &Comment{}
|
||||
|
|
@ -167,7 +166,8 @@ func (r Range) Before(r2 Range) bool {
|
|||
type Position struct {
|
||||
Line int
|
||||
Column int
|
||||
Byte int
|
||||
// -1 is used as sentinel that a constructed position is missing byte offset (for LSP usage)
|
||||
Byte int
|
||||
}
|
||||
|
||||
var _ fmt.Stringer = Position{}
|
||||
|
|
@ -277,7 +277,13 @@ func (p Position) SubtractString(s string, byUTF16 bool) Position {
|
|||
}
|
||||
|
||||
func (p Position) Before(p2 Position) bool {
|
||||
return p.Byte < p2.Byte
|
||||
if p.Byte != p2.Byte && p.Byte != -1 && p2.Byte != -1 {
|
||||
return p.Byte < p2.Byte
|
||||
}
|
||||
if p.Line != p2.Line {
|
||||
return p.Line < p2.Line
|
||||
}
|
||||
return p.Column < p2.Column
|
||||
}
|
||||
|
||||
// MapNode is implemented by nodes that may be children of Maps.
|
||||
|
|
@ -332,6 +338,7 @@ type String interface {
|
|||
SetString(string)
|
||||
Copy() String
|
||||
_string()
|
||||
IsUnquoted() bool
|
||||
}
|
||||
|
||||
var _ String = &UnquotedString{}
|
||||
|
|
@ -432,6 +439,139 @@ func (s *DoubleQuotedString) scalar() {}
|
|||
func (s *SingleQuotedString) scalar() {}
|
||||
func (s *BlockString) scalar() {}
|
||||
|
||||
func (c *Comment) Children() []Node { return nil }
|
||||
func (c *BlockComment) Children() []Node { return nil }
|
||||
func (n *Null) Children() []Node { return nil }
|
||||
func (b *Boolean) Children() []Node { return nil }
|
||||
func (n *Number) Children() []Node { return nil }
|
||||
func (s *SingleQuotedString) Children() []Node { return nil }
|
||||
func (s *BlockString) Children() []Node { return nil }
|
||||
func (ei *EdgeIndex) Children() []Node { return nil }
|
||||
|
||||
func (s *UnquotedString) Children() []Node {
|
||||
var children []Node
|
||||
for _, box := range s.Value {
|
||||
if box.Substitution != nil {
|
||||
children = append(children, box.Substitution)
|
||||
}
|
||||
}
|
||||
return children
|
||||
}
|
||||
|
||||
func (s *DoubleQuotedString) Children() []Node {
|
||||
var children []Node
|
||||
for _, box := range s.Value {
|
||||
if box.Substitution != nil {
|
||||
children = append(children, box.Substitution)
|
||||
}
|
||||
}
|
||||
return children
|
||||
}
|
||||
|
||||
func (s *Substitution) Children() []Node {
|
||||
var children []Node
|
||||
for _, sb := range s.Path {
|
||||
if sb != nil {
|
||||
if child := sb.Unbox(); child != nil {
|
||||
children = append(children, child)
|
||||
}
|
||||
}
|
||||
}
|
||||
return children
|
||||
}
|
||||
|
||||
func (i *Import) Children() []Node {
|
||||
var children []Node
|
||||
for _, sb := range i.Path {
|
||||
if sb != nil {
|
||||
if child := sb.Unbox(); child != nil {
|
||||
children = append(children, child)
|
||||
}
|
||||
}
|
||||
}
|
||||
return children
|
||||
}
|
||||
|
||||
func (a *Array) Children() []Node {
|
||||
var children []Node
|
||||
for _, box := range a.Nodes {
|
||||
if child := box.Unbox(); child != nil {
|
||||
children = append(children, child)
|
||||
}
|
||||
}
|
||||
return children
|
||||
}
|
||||
|
||||
func (m *Map) Children() []Node {
|
||||
var children []Node
|
||||
for _, box := range m.Nodes {
|
||||
if child := box.Unbox(); child != nil {
|
||||
children = append(children, child)
|
||||
}
|
||||
}
|
||||
return children
|
||||
}
|
||||
|
||||
func (k *Key) Children() []Node {
|
||||
var children []Node
|
||||
if k.Key != nil {
|
||||
children = append(children, k.Key)
|
||||
}
|
||||
for _, edge := range k.Edges {
|
||||
if edge != nil {
|
||||
children = append(children, edge)
|
||||
}
|
||||
}
|
||||
if k.EdgeIndex != nil {
|
||||
children = append(children, k.EdgeIndex)
|
||||
}
|
||||
if k.EdgeKey != nil {
|
||||
children = append(children, k.EdgeKey)
|
||||
}
|
||||
if scalar := k.Primary.Unbox(); scalar != nil {
|
||||
children = append(children, scalar)
|
||||
}
|
||||
if value := k.Value.Unbox(); value != nil {
|
||||
children = append(children, value)
|
||||
}
|
||||
return children
|
||||
}
|
||||
|
||||
func (kp *KeyPath) Children() []Node {
|
||||
var children []Node
|
||||
for _, sb := range kp.Path {
|
||||
if sb != nil {
|
||||
if child := sb.Unbox(); child != nil {
|
||||
children = append(children, child)
|
||||
}
|
||||
}
|
||||
}
|
||||
return children
|
||||
}
|
||||
|
||||
func (e *Edge) Children() []Node {
|
||||
var children []Node
|
||||
if e.Src != nil {
|
||||
children = append(children, e.Src)
|
||||
}
|
||||
if e.Dst != nil {
|
||||
children = append(children, e.Dst)
|
||||
}
|
||||
return children
|
||||
}
|
||||
|
||||
func Walk(node Node, fn func(Node) bool) {
|
||||
if node == nil {
|
||||
return
|
||||
}
|
||||
if !fn(node) {
|
||||
return
|
||||
}
|
||||
for _, child := range node.Children() {
|
||||
Walk(child, fn)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: mistake, move into parse.go
|
||||
func (n *Null) ScalarString() string { return "" }
|
||||
func (b *Boolean) ScalarString() string { return strconv.FormatBool(b.Value) }
|
||||
|
|
@ -472,6 +612,11 @@ func (s *DoubleQuotedString) _string() {}
|
|||
func (s *SingleQuotedString) _string() {}
|
||||
func (s *BlockString) _string() {}
|
||||
|
||||
func (s *UnquotedString) IsUnquoted() bool { return true }
|
||||
func (s *DoubleQuotedString) IsUnquoted() bool { return false }
|
||||
func (s *SingleQuotedString) IsUnquoted() bool { return false }
|
||||
func (s *BlockString) IsUnquoted() bool { return false }
|
||||
|
||||
type Comment struct {
|
||||
Range Range `json:"range"`
|
||||
Value string `json:"value"`
|
||||
|
|
@ -660,6 +805,9 @@ func (mk1 *Key) D2OracleEquals(mk2 *Key) bool {
|
|||
if mk1.Ampersand != mk2.Ampersand {
|
||||
return false
|
||||
}
|
||||
if mk1.NotAmpersand != mk2.NotAmpersand {
|
||||
return false
|
||||
}
|
||||
if (mk1.Key == nil) != (mk2.Key == nil) {
|
||||
return false
|
||||
}
|
||||
|
|
@ -739,6 +887,9 @@ func (mk1 *Key) Equals(mk2 *Key) bool {
|
|||
if mk1.Ampersand != mk2.Ampersand {
|
||||
return false
|
||||
}
|
||||
if mk1.NotAmpersand != mk2.NotAmpersand {
|
||||
return false
|
||||
}
|
||||
if (mk1.Key == nil) != (mk2.Key == nil) {
|
||||
return false
|
||||
}
|
||||
|
|
@ -862,10 +1013,22 @@ func (mk *Key) HasTripleGlob() bool {
|
|||
return true
|
||||
}
|
||||
}
|
||||
if mk.EdgeIndex != nil && mk.EdgeIndex.Glob {
|
||||
if mk.EdgeKey.HasTripleGlob() {
|
||||
return true
|
||||
}
|
||||
if mk.EdgeKey.HasTripleGlob() {
|
||||
return false
|
||||
}
|
||||
|
||||
func (mk *Key) HasMultiGlob() bool {
|
||||
if mk.Key.HasMultiGlob() {
|
||||
return true
|
||||
}
|
||||
for _, e := range mk.Edges {
|
||||
if e.Src.HasMultiGlob() || e.Dst.HasMultiGlob() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if mk.EdgeKey.HasMultiGlob() {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
|
@ -902,7 +1065,22 @@ func MakeKeyPath(a []string) *KeyPath {
|
|||
return kp
|
||||
}
|
||||
|
||||
func (kp *KeyPath) IDA() (ida []string) {
|
||||
func MakeKeyPathString(a []String) *KeyPath {
|
||||
kp := &KeyPath{}
|
||||
for _, el := range a {
|
||||
kp.Path = append(kp.Path, MakeValueBox(RawString(el.ScalarString(), true)).StringBox())
|
||||
}
|
||||
return kp
|
||||
}
|
||||
|
||||
func (kp *KeyPath) IDA() (ida []String) {
|
||||
for _, el := range kp.Path {
|
||||
ida = append(ida, el.Unbox())
|
||||
}
|
||||
return ida
|
||||
}
|
||||
|
||||
func (kp *KeyPath) StringIDA() (ida []string) {
|
||||
for _, el := range kp.Path {
|
||||
ida = append(ida, el.Unbox().ScalarString())
|
||||
}
|
||||
|
|
@ -1415,9 +1593,9 @@ func (s *Substitution) IDA() (ida []string) {
|
|||
return ida
|
||||
}
|
||||
|
||||
func (i *Import) IDA() (ida []string) {
|
||||
func (i *Import) IDA() (ida []String) {
|
||||
for _, el := range i.Path[1:] {
|
||||
ida = append(ida, el.Unbox().ScalarString())
|
||||
ida = append(ida, el.Unbox())
|
||||
}
|
||||
return ida
|
||||
}
|
||||
|
|
@ -1428,3 +1606,7 @@ func (i *Import) PathWithPre() string {
|
|||
}
|
||||
return path.Join(i.Pre, i.Path[0].Unbox().ScalarString())
|
||||
}
|
||||
|
||||
func (i *Import) Dir() string {
|
||||
return path.Dir(i.PathWithPre())
|
||||
}
|
||||
|
|
|
|||
200
d2ast/keywords.go
Normal file
200
d2ast/keywords.go
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
package d2ast
|
||||
|
||||
import "oss.terrastruct.com/d2/lib/label"
|
||||
|
||||
// All reserved keywords. See init below.
|
||||
var ReservedKeywords map[string]struct{}
|
||||
|
||||
// Non Style/Holder keywords.
|
||||
var SimpleReservedKeywords = map[string]struct{}{
|
||||
"label": {},
|
||||
"shape": {},
|
||||
"icon": {},
|
||||
"constraint": {},
|
||||
"tooltip": {},
|
||||
"link": {},
|
||||
"near": {},
|
||||
"width": {},
|
||||
"height": {},
|
||||
"direction": {},
|
||||
"top": {},
|
||||
"left": {},
|
||||
"grid-rows": {},
|
||||
"grid-columns": {},
|
||||
"grid-gap": {},
|
||||
"vertical-gap": {},
|
||||
"horizontal-gap": {},
|
||||
"class": {},
|
||||
"vars": {},
|
||||
}
|
||||
|
||||
// ReservedKeywordHolders are reserved keywords that are meaningless on its own and must hold composites
|
||||
var ReservedKeywordHolders = map[string]struct{}{
|
||||
"style": {},
|
||||
}
|
||||
|
||||
// CompositeReservedKeywords are reserved keywords that can hold composites
|
||||
var CompositeReservedKeywords = map[string]struct{}{
|
||||
"source-arrowhead": {},
|
||||
"target-arrowhead": {},
|
||||
"classes": {},
|
||||
"constraint": {},
|
||||
"label": {},
|
||||
"icon": {},
|
||||
}
|
||||
|
||||
// StyleKeywords are reserved keywords which cannot exist outside of the "style" keyword
|
||||
var StyleKeywords = map[string]struct{}{
|
||||
"opacity": {},
|
||||
"stroke": {},
|
||||
"fill": {},
|
||||
"fill-pattern": {},
|
||||
"stroke-width": {},
|
||||
"stroke-dash": {},
|
||||
"border-radius": {},
|
||||
|
||||
// Only for text
|
||||
"font": {},
|
||||
"font-size": {},
|
||||
"font-color": {},
|
||||
"bold": {},
|
||||
"italic": {},
|
||||
"underline": {},
|
||||
"text-transform": {},
|
||||
|
||||
// Only for shapes
|
||||
"shadow": {},
|
||||
"multiple": {},
|
||||
"double-border": {},
|
||||
|
||||
// Only for squares
|
||||
"3d": {},
|
||||
|
||||
// Only for edges
|
||||
"animated": {},
|
||||
"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{}
|
||||
|
||||
// LabelPositionsArray are the values that labels and icons can set `near` to
|
||||
var LabelPositionsArray = []string{
|
||||
"top-left",
|
||||
"top-center",
|
||||
"top-right",
|
||||
|
||||
"center-left",
|
||||
"center-center",
|
||||
"center-right",
|
||||
|
||||
"bottom-left",
|
||||
"bottom-center",
|
||||
"bottom-right",
|
||||
|
||||
"outside-top-left",
|
||||
"outside-top-center",
|
||||
"outside-top-right",
|
||||
|
||||
"outside-left-top",
|
||||
"outside-left-center",
|
||||
"outside-left-bottom",
|
||||
|
||||
"outside-right-top",
|
||||
"outside-right-center",
|
||||
"outside-right-bottom",
|
||||
|
||||
"outside-bottom-left",
|
||||
"outside-bottom-center",
|
||||
"outside-bottom-right",
|
||||
}
|
||||
var LabelPositions map[string]struct{}
|
||||
|
||||
var LabelPositionsMapping = map[string]label.Position{
|
||||
"top-left": label.InsideTopLeft,
|
||||
"top-center": label.InsideTopCenter,
|
||||
"top-right": label.InsideTopRight,
|
||||
|
||||
"center-left": label.InsideMiddleLeft,
|
||||
"center-center": label.InsideMiddleCenter,
|
||||
"center-right": label.InsideMiddleRight,
|
||||
|
||||
"bottom-left": label.InsideBottomLeft,
|
||||
"bottom-center": label.InsideBottomCenter,
|
||||
"bottom-right": label.InsideBottomRight,
|
||||
|
||||
"outside-top-left": label.OutsideTopLeft,
|
||||
"outside-top-center": label.OutsideTopCenter,
|
||||
"outside-top-right": label.OutsideTopRight,
|
||||
|
||||
"outside-left-top": label.OutsideLeftTop,
|
||||
"outside-left-center": label.OutsideLeftMiddle,
|
||||
"outside-left-bottom": label.OutsideLeftBottom,
|
||||
|
||||
"outside-right-top": label.OutsideRightTop,
|
||||
"outside-right-center": label.OutsideRightMiddle,
|
||||
"outside-right-bottom": label.OutsideRightBottom,
|
||||
|
||||
"outside-bottom-left": label.OutsideBottomLeft,
|
||||
"outside-bottom-center": label.OutsideBottomCenter,
|
||||
"outside-bottom-right": label.OutsideBottomRight,
|
||||
}
|
||||
|
||||
var FillPatterns = []string{
|
||||
"none",
|
||||
"dots",
|
||||
"lines",
|
||||
"grain",
|
||||
"paper",
|
||||
}
|
||||
|
||||
var TextTransforms = []string{"none", "uppercase", "lowercase", "capitalize"}
|
||||
|
||||
// BoardKeywords contains the keywords that create new boards.
|
||||
var BoardKeywords = map[string]struct{}{
|
||||
"layers": {},
|
||||
"scenarios": {},
|
||||
"steps": {},
|
||||
}
|
||||
|
||||
func init() {
|
||||
ReservedKeywords = make(map[string]struct{})
|
||||
for k, v := range SimpleReservedKeywords {
|
||||
ReservedKeywords[k] = v
|
||||
}
|
||||
for k, v := range StyleKeywords {
|
||||
ReservedKeywords[k] = v
|
||||
}
|
||||
for k, v := range ReservedKeywordHolders {
|
||||
CompositeReservedKeywords[k] = v
|
||||
}
|
||||
for k, v := range BoardKeywords {
|
||||
CompositeReservedKeywords[k] = v
|
||||
}
|
||||
for k, v := range CompositeReservedKeywords {
|
||||
ReservedKeywords[k] = v
|
||||
}
|
||||
|
||||
NearConstants = make(map[string]struct{}, len(NearConstantsArray))
|
||||
for _, k := range NearConstantsArray {
|
||||
NearConstants[k] = struct{}{}
|
||||
}
|
||||
|
||||
LabelPositions = make(map[string]struct{}, len(LabelPositionsArray))
|
||||
for _, k := range LabelPositionsArray {
|
||||
LabelPositions[k] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
|
@ -269,7 +269,7 @@ func (gs *dslGenState) randStr(n int, inKey bool) string {
|
|||
func (gs *dslGenState) randShape() string {
|
||||
for {
|
||||
s := shapes[gs.rand.Intn(len(shapes))]
|
||||
if s != d2target.ShapeImage {
|
||||
if s != d2target.ShapeImage && s != d2target.ShapeText {
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ func test(t *testing.T, textPath, text string) {
|
|||
}
|
||||
}()
|
||||
|
||||
ctx := log.WithTB(context.Background(), t, nil)
|
||||
ctx := log.WithTB(context.Background(), t)
|
||||
|
||||
ruler, err := textmeasure.NewRuler()
|
||||
assert.Nil(t, err)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
package d2cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type exportExtension string
|
||||
|
|
@ -14,6 +16,24 @@ const SVG exportExtension = ".svg"
|
|||
|
||||
var SUPPORTED_EXTENSIONS = []exportExtension{SVG, PNG, PDF, PPTX, GIF}
|
||||
|
||||
var STDOUT_FORMAT_MAP = map[string]exportExtension{
|
||||
"png": PNG,
|
||||
"svg": SVG,
|
||||
}
|
||||
|
||||
var SUPPORTED_STDOUT_FORMATS = []string{"png", "svg"}
|
||||
|
||||
func getOutputFormat(stdoutFormatFlag *string, outputPath string) (exportExtension, error) {
|
||||
if stdoutFormatFlag != nil && *stdoutFormatFlag != "" {
|
||||
format := strings.ToLower(*stdoutFormatFlag)
|
||||
if ext, ok := STDOUT_FORMAT_MAP[format]; ok {
|
||||
return ext, nil
|
||||
}
|
||||
return "", fmt.Errorf("%s is not a supported format. Supported formats are: %s", *stdoutFormatFlag, SUPPORTED_STDOUT_FORMATS)
|
||||
}
|
||||
return getExportExtension(outputPath), nil
|
||||
}
|
||||
|
||||
func getExportExtension(outputPath string) exportExtension {
|
||||
ext := filepath.Ext(outputPath)
|
||||
for _, kext := range SUPPORTED_EXTENSIONS {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import (
|
|||
|
||||
func TestOutputFormat(t *testing.T) {
|
||||
type testCase struct {
|
||||
stdoutFormatFlag string
|
||||
outputPath string
|
||||
extension exportExtension
|
||||
supportsDarkTheme bool
|
||||
|
|
@ -41,6 +42,15 @@ func TestOutputFormat(t *testing.T) {
|
|||
requiresAnimationInterval: false,
|
||||
requiresPngRender: false,
|
||||
},
|
||||
{
|
||||
stdoutFormatFlag: "png",
|
||||
outputPath: "-",
|
||||
extension: PNG,
|
||||
supportsDarkTheme: false,
|
||||
supportsAnimation: false,
|
||||
requiresAnimationInterval: false,
|
||||
requiresPngRender: true,
|
||||
},
|
||||
{
|
||||
outputPath: "/out.png",
|
||||
extension: PNG,
|
||||
|
|
@ -78,7 +88,8 @@ func TestOutputFormat(t *testing.T) {
|
|||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
t.Run(tc.outputPath, func(t *testing.T) {
|
||||
extension := getExportExtension(tc.outputPath)
|
||||
extension, err := getOutputFormat(&tc.stdoutFormatFlag, tc.outputPath)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tc.extension, extension)
|
||||
assert.Equal(t, tc.supportsAnimation, extension.supportsAnimation())
|
||||
assert.Equal(t, tc.supportsDarkTheme, extension.supportsDarkTheme())
|
||||
|
|
|
|||
24
d2cli/fmt.go
24
d2cli/fmt.go
|
|
@ -12,9 +12,10 @@ import (
|
|||
|
||||
"oss.terrastruct.com/d2/d2format"
|
||||
"oss.terrastruct.com/d2/d2parser"
|
||||
"oss.terrastruct.com/d2/lib/log"
|
||||
)
|
||||
|
||||
func fmtCmd(ctx context.Context, ms *xmain.State) (err error) {
|
||||
func fmtCmd(ctx context.Context, ms *xmain.State, check bool) (err error) {
|
||||
defer xdefer.Errorf(&err, "failed to fmt")
|
||||
|
||||
ms.Opts = xmain.NewOpts(ms.Env, ms.Opts.Flags.Args()[1:])
|
||||
|
|
@ -22,6 +23,8 @@ func fmtCmd(ctx context.Context, ms *xmain.State) (err error) {
|
|||
return xmain.UsageErrorf("fmt must be passed at least one file to be formatted")
|
||||
}
|
||||
|
||||
unformattedCount := 0
|
||||
|
||||
for _, inputPath := range ms.Opts.Args {
|
||||
if inputPath != "-" {
|
||||
inputPath = ms.AbsPath(inputPath)
|
||||
|
|
@ -43,10 +46,25 @@ func fmtCmd(ctx context.Context, ms *xmain.State) (err error) {
|
|||
|
||||
output := []byte(d2format.Format(m))
|
||||
if !bytes.Equal(output, input) {
|
||||
if err := ms.WritePath(inputPath, output); err != nil {
|
||||
return err
|
||||
if check {
|
||||
unformattedCount += 1
|
||||
log.Warn(ctx, inputPath)
|
||||
} else {
|
||||
if err := ms.WritePath(inputPath, output); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if unformattedCount > 0 {
|
||||
pluralFiles := "file"
|
||||
if unformattedCount > 1 {
|
||||
pluralFiles = "files"
|
||||
}
|
||||
|
||||
return xmain.ExitErrorf(1, "found %d unformatted %s. Run d2 fmt to fix.", unformattedCount, pluralFiles)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ Usage:
|
|||
%[1]s [--watch=false] [--theme=0] file.d2 [file.svg | file.png]
|
||||
%[1]s layout [name]
|
||||
%[1]s fmt file.d2 ...
|
||||
%[1]s play [--theme=0] [--sketch] file.d2
|
||||
|
||||
%[1]s compiles and renders file.d2 to file.svg | file.png
|
||||
It defaults to file.svg if an output path is not provided.
|
||||
|
|
@ -38,6 +39,7 @@ Subcommands:
|
|||
%[1]s layout [name] - Display long help for a particular layout engine, including its configuration options
|
||||
%[1]s themes - Lists available themes
|
||||
%[1]s fmt file.d2 ... - Format passed files
|
||||
%[1]s play file.d2 - Opens the file in playground, an online web viewer (https://play.d2lang.com)
|
||||
|
||||
See more docs and the source code at https://oss.terrastruct.com/d2.
|
||||
Hosted icons at https://icons.terrastruct.com.
|
||||
|
|
|
|||
230
d2cli/main.go
230
d2cli/main.go
|
|
@ -5,8 +5,8 @@ import (
|
|||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"log/slog"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
|
|
@ -36,7 +36,7 @@ import (
|
|||
"oss.terrastruct.com/d2/d2themes/d2themescatalog"
|
||||
"oss.terrastruct.com/d2/lib/background"
|
||||
"oss.terrastruct.com/d2/lib/imgbundler"
|
||||
ctxlog "oss.terrastruct.com/d2/lib/log"
|
||||
"oss.terrastruct.com/d2/lib/log"
|
||||
"oss.terrastruct.com/d2/lib/pdf"
|
||||
"oss.terrastruct.com/d2/lib/png"
|
||||
"oss.terrastruct.com/d2/lib/pptx"
|
||||
|
|
@ -45,15 +45,10 @@ import (
|
|||
timelib "oss.terrastruct.com/d2/lib/time"
|
||||
"oss.terrastruct.com/d2/lib/version"
|
||||
"oss.terrastruct.com/d2/lib/xgif"
|
||||
|
||||
"cdr.dev/slog"
|
||||
"cdr.dev/slog/sloggers/sloghuman"
|
||||
)
|
||||
|
||||
func Run(ctx context.Context, ms *xmain.State) (err error) {
|
||||
// :(
|
||||
ctx = DiscardSlog(ctx)
|
||||
|
||||
ctx = log.WithDefault(ctx)
|
||||
// These should be kept up-to-date with the d2 man page
|
||||
watchFlag, err := ms.Opts.Bool("D2_WATCH", "watch", "w", false, "watch for changes to input and live reload. Use $HOST and $PORT to specify the listening address.\n(default localhost:0, which is will open on a randomly available local port).")
|
||||
if err != nil {
|
||||
|
|
@ -108,6 +103,11 @@ func Run(ctx context.Context, ms *xmain.State) (err error) {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stdoutFormatFlag := ms.Opts.String("", "stdout-format", "", "", "output format when writing to stdout (svg, png). Usage: d2 input.d2 --stdout-format png - > output.png")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
browserFlag := ms.Opts.String("BROWSER", "browser", "", "", "browser executable that watch opens. Setting to 0 opens no browser.")
|
||||
centerFlag, err := ms.Opts.Bool("D2_CENTER", "center", "c", false, "center the SVG in the containing viewbox, such as your browser screen")
|
||||
if err != nil {
|
||||
|
|
@ -124,6 +124,18 @@ func Run(ctx context.Context, ms *xmain.State) (err error) {
|
|||
fontBoldFlag := ms.Opts.String("D2_FONT_BOLD", "font-bold", "", "", "path to .ttf file to use for the bold font. If none provided, Source Sans Pro Bold is used.")
|
||||
fontSemiboldFlag := ms.Opts.String("D2_FONT_SEMIBOLD", "font-semibold", "", "", "path to .ttf file to use for the semibold font. If none provided, Source Sans Pro Semibold is used.")
|
||||
|
||||
checkFlag, err := ms.Opts.Bool("D2_CHECK", "check", "", false, "check that the specified files are formatted correctly.")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
noXMLTagFlag, err := ms.Opts.Bool("D2_NO_XML_TAG", "no-xml-tag", "", false, "omit XML tag (<?xml ...?>) from output SVG files. Useful when generating SVGs for direct HTML embedding")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
saltFlag := ms.Opts.String("", "salt", "", "", "Add a salt value to ensure the output uses unique IDs. This is useful when generating multiple identical diagrams to be included in the same HTML doc, so that duplicate IDs do not cause invalid HTML. The salt value is a string that will be appended to IDs in the output.")
|
||||
|
||||
plugins, err := d2plugin.ListPlugins(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -158,7 +170,9 @@ func Run(ctx context.Context, ms *xmain.State) (err error) {
|
|||
themesCmd(ctx, ms)
|
||||
return nil
|
||||
case "fmt":
|
||||
return fmtCmd(ctx, ms)
|
||||
return fmtCmd(ctx, ms, *checkFlag)
|
||||
case "play":
|
||||
return playCmd(ctx, ms)
|
||||
case "version":
|
||||
if len(ms.Opts.Flags.Args()) > 1 {
|
||||
return xmain.UsageErrorf("version subcommand accepts no arguments")
|
||||
|
|
@ -169,6 +183,7 @@ func Run(ctx context.Context, ms *xmain.State) (err error) {
|
|||
}
|
||||
|
||||
if *debugFlag {
|
||||
ctx = log.Leveled(ctx, slog.LevelDebug)
|
||||
ms.Env.Setenv("DEBUG", "1")
|
||||
}
|
||||
if *imgCacheFlag {
|
||||
|
|
@ -217,7 +232,12 @@ func Run(ctx context.Context, ms *xmain.State) (err error) {
|
|||
if filepath.Ext(outputPath) == ".ppt" {
|
||||
return xmain.UsageErrorf("D2 does not support ppt exports, did you mean \"pptx\"?")
|
||||
}
|
||||
outputFormat := getExportExtension(outputPath)
|
||||
|
||||
outputFormat, err := getOutputFormat(stdoutFormatFlag, outputPath)
|
||||
if err != nil {
|
||||
return xmain.UsageErrorf("%v", err)
|
||||
}
|
||||
|
||||
if outputPath != "-" {
|
||||
outputPath = ms.AbsPath(outputPath)
|
||||
if *animateIntervalFlag > 0 && !outputFormat.supportsAnimation() {
|
||||
|
|
@ -307,6 +327,8 @@ func Run(ctx context.Context, ms *xmain.State) (err error) {
|
|||
ThemeID: themeFlag,
|
||||
DarkThemeID: darkThemeFlag,
|
||||
Scale: scale,
|
||||
NoXMLTag: noXMLTagFlag,
|
||||
Salt: saltFlag,
|
||||
}
|
||||
|
||||
if *watchFlag {
|
||||
|
|
@ -329,6 +351,7 @@ func Run(ctx context.Context, ms *xmain.State) (err error) {
|
|||
forceAppendix: *forceAppendixFlag,
|
||||
pw: pw,
|
||||
fontFamily: fontFamily,
|
||||
outputFormat: outputFormat,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -353,13 +376,13 @@ func Run(ctx context.Context, ms *xmain.State) (err error) {
|
|||
if err != nil {
|
||||
return xmain.UsageErrorf("invalid target: %s", *targetFlag)
|
||||
}
|
||||
boardPath = key.IDA()
|
||||
boardPath = key.StringIDA()
|
||||
}
|
||||
|
||||
ctx, cancel := timelib.WithTimeout(ctx, time.Minute*2)
|
||||
defer cancel()
|
||||
|
||||
_, written, err := compile(ctx, ms, plugins, nil, layoutFlag, renderOpts, fontFamily, *animateIntervalFlag, inputPath, outputPath, boardPath, noChildren, *bundleFlag, *forceAppendixFlag, pw.Page)
|
||||
_, written, err := compile(ctx, ms, plugins, nil, layoutFlag, renderOpts, fontFamily, *animateIntervalFlag, inputPath, outputPath, boardPath, noChildren, *bundleFlag, *forceAppendixFlag, pw.Page, outputFormat)
|
||||
if err != nil {
|
||||
if written {
|
||||
return fmt.Errorf("failed to fully compile (partial render written) %s: %w", ms.HumanPath(inputPath), err)
|
||||
|
|
@ -434,7 +457,7 @@ func RouterResolver(ctx context.Context, ms *xmain.State, plugins []d2plugin.Plu
|
|||
}
|
||||
}
|
||||
|
||||
func compile(ctx context.Context, ms *xmain.State, plugins []d2plugin.Plugin, fs fs.FS, layout *string, renderOpts d2svg.RenderOpts, fontFamily *d2fonts.FontFamily, animateInterval int64, inputPath, outputPath string, boardPath []string, noChildren, bundle, forceAppendix bool, page playwright.Page) (_ []byte, written bool, _ error) {
|
||||
func compile(ctx context.Context, ms *xmain.State, plugins []d2plugin.Plugin, fs fs.FS, layout *string, renderOpts d2svg.RenderOpts, fontFamily *d2fonts.FontFamily, animateInterval int64, inputPath, outputPath string, boardPath []string, noChildren, bundle, forceAppendix bool, page playwright.Page, ext exportExtension) (_ []byte, written bool, _ error) {
|
||||
start := time.Now()
|
||||
input, err := ms.ReadPath(inputPath)
|
||||
if err != nil {
|
||||
|
|
@ -459,7 +482,7 @@ func compile(ctx context.Context, ms *xmain.State, plugins []d2plugin.Plugin, fs
|
|||
if os.Getenv("D2_LSP_MODE") == "1" {
|
||||
// only the parse result is needed if running d2 for lsp,
|
||||
// if this, "fails", the AST is still valid and can be sent
|
||||
// to vscode extention
|
||||
// to vscode extension
|
||||
ast, err := d2lib.Parse(ctx, string(input), opts)
|
||||
|
||||
type LspOutputData struct {
|
||||
|
|
@ -480,13 +503,13 @@ func compile(ctx context.Context, ms *xmain.State, plugins []d2plugin.Plugin, fs
|
|||
}, time.Second*5)
|
||||
defer cancel()
|
||||
|
||||
diagram, g, err := d2lib.Compile(ctx, string(input), opts, &renderOpts)
|
||||
rootDiagram, g, err := d2lib.Compile(ctx, string(input), opts, &renderOpts)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
cancel()
|
||||
|
||||
diagram = diagram.GetBoard(boardPath)
|
||||
diagram := rootDiagram.GetBoard(boardPath)
|
||||
if diagram == nil {
|
||||
return nil, false, fmt.Errorf(`render target "%s" not found`, strings.Join(boardPath, "."))
|
||||
}
|
||||
|
|
@ -499,7 +522,7 @@ func compile(ctx context.Context, ms *xmain.State, plugins []d2plugin.Plugin, fs
|
|||
plugin, _ := d2plugin.FindPlugin(ctx, plugins, *opts.Layout)
|
||||
|
||||
if animateInterval > 0 {
|
||||
masterID, err := diagram.HashID()
|
||||
masterID, err := diagram.HashID(renderOpts.Salt)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
|
@ -526,10 +549,9 @@ func compile(ctx context.Context, ms *xmain.State, plugins []d2plugin.Plugin, fs
|
|||
return nil, false, err
|
||||
}
|
||||
|
||||
ext := getExportExtension(outputPath)
|
||||
switch ext {
|
||||
case GIF:
|
||||
svg, pngs, err := renderPNGsForGIF(ctx, ms, plugin, renderOpts, ruler, page, diagram)
|
||||
svg, pngs, err := renderPNGsForGIF(ctx, ms, plugin, renderOpts, ruler, page, inputPath, diagram)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
|
@ -541,7 +563,7 @@ func compile(ctx context.Context, ms *xmain.State, plugins []d2plugin.Plugin, fs
|
|||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
err = ms.WritePath(outputPath, out)
|
||||
err = Write(ms, outputPath, out)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
|
@ -553,7 +575,7 @@ func compile(ctx context.Context, ms *xmain.State, plugins []d2plugin.Plugin, fs
|
|||
path := []pdf.BoardTitle{
|
||||
{Name: diagram.Root.Label, BoardID: "root"},
|
||||
}
|
||||
pdf, err := renderPDF(ctx, ms, plugin, renderOpts, outputPath, page, ruler, diagram, nil, path, pageMap, diagram.Root.Label != "")
|
||||
pdf, err := renderPDF(ctx, ms, plugin, renderOpts, inputPath, outputPath, page, ruler, diagram, nil, path, pageMap, diagram.Root.Label != "")
|
||||
if err != nil {
|
||||
return pdf, false, err
|
||||
}
|
||||
|
|
@ -574,7 +596,7 @@ func compile(ctx context.Context, ms *xmain.State, plugins []d2plugin.Plugin, fs
|
|||
path := []pptx.BoardTitle{
|
||||
{Name: "root", BoardID: "root", LinkToSlide: boardIdToIndex["root"] + 1},
|
||||
}
|
||||
svg, err := renderPPTX(ctx, ms, p, plugin, renderOpts, ruler, outputPath, page, diagram, path, boardIdToIndex)
|
||||
svg, err := renderPPTX(ctx, ms, p, plugin, renderOpts, ruler, inputPath, outputPath, page, diagram, path, boardIdToIndex)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
|
@ -589,11 +611,11 @@ func compile(ctx context.Context, ms *xmain.State, plugins []d2plugin.Plugin, fs
|
|||
compileDur := time.Since(start)
|
||||
if animateInterval <= 0 {
|
||||
// Rename all the "root.layers.x" to the paths that the boards get output to
|
||||
linkToOutput, err := resolveLinks("root", outputPath, diagram)
|
||||
linkToOutput, err := resolveLinks("root", outputPath, rootDiagram)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
err = relink("root", diagram, linkToOutput)
|
||||
err = relink("root", rootDiagram, linkToOutput)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
|
@ -602,9 +624,9 @@ func compile(ctx context.Context, ms *xmain.State, plugins []d2plugin.Plugin, fs
|
|||
var boards [][]byte
|
||||
var err error
|
||||
if noChildren {
|
||||
boards, err = renderSingle(ctx, ms, compileDur, plugin, renderOpts, inputPath, outputPath, bundle, forceAppendix, page, ruler, diagram)
|
||||
boards, err = renderSingle(ctx, ms, compileDur, plugin, renderOpts, inputPath, outputPath, bundle, forceAppendix, page, ruler, diagram, ext)
|
||||
} else {
|
||||
boards, err = render(ctx, ms, compileDur, plugin, renderOpts, inputPath, outputPath, bundle, forceAppendix, page, ruler, diagram)
|
||||
boards, err = render(ctx, ms, compileDur, plugin, renderOpts, inputPath, outputPath, bundle, forceAppendix, page, ruler, diagram, ext)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
|
|
@ -617,11 +639,15 @@ func compile(ctx context.Context, ms *xmain.State, plugins []d2plugin.Plugin, fs
|
|||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
out, err = plugin.PostProcess(ctx, out)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
err = os.MkdirAll(filepath.Dir(outputPath), 0755)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
err = ms.WritePath(outputPath, out)
|
||||
err = Write(ms, outputPath, out)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
|
@ -739,7 +765,7 @@ func relink(currDiagramPath string, d *d2target.Diagram, linkToOutput map[string
|
|||
return nil
|
||||
}
|
||||
|
||||
func render(ctx context.Context, ms *xmain.State, compileDur time.Duration, plugin d2plugin.Plugin, opts d2svg.RenderOpts, inputPath, outputPath string, bundle, forceAppendix bool, page playwright.Page, ruler *textmeasure.Ruler, diagram *d2target.Diagram) ([][]byte, error) {
|
||||
func render(ctx context.Context, ms *xmain.State, compileDur time.Duration, plugin d2plugin.Plugin, opts d2svg.RenderOpts, inputPath, outputPath string, bundle, forceAppendix bool, page playwright.Page, ruler *textmeasure.Ruler, diagram *d2target.Diagram, ext exportExtension) ([][]byte, error) {
|
||||
if diagram.Name != "" {
|
||||
ext := filepath.Ext(outputPath)
|
||||
outputPath = strings.TrimSuffix(outputPath, ext)
|
||||
|
|
@ -785,21 +811,21 @@ func render(ctx context.Context, ms *xmain.State, compileDur time.Duration, plug
|
|||
|
||||
var boards [][]byte
|
||||
for _, dl := range diagram.Layers {
|
||||
childrenBoards, err := render(ctx, ms, compileDur, plugin, opts, inputPath, layersOutputPath, bundle, forceAppendix, page, ruler, dl)
|
||||
childrenBoards, err := render(ctx, ms, compileDur, plugin, opts, inputPath, layersOutputPath, bundle, forceAppendix, page, ruler, dl, ext)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
boards = append(boards, childrenBoards...)
|
||||
}
|
||||
for _, dl := range diagram.Scenarios {
|
||||
childrenBoards, err := render(ctx, ms, compileDur, plugin, opts, inputPath, scenariosOutputPath, bundle, forceAppendix, page, ruler, dl)
|
||||
childrenBoards, err := render(ctx, ms, compileDur, plugin, opts, inputPath, scenariosOutputPath, bundle, forceAppendix, page, ruler, dl, ext)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
boards = append(boards, childrenBoards...)
|
||||
}
|
||||
for _, dl := range diagram.Steps {
|
||||
childrenBoards, err := render(ctx, ms, compileDur, plugin, opts, inputPath, stepsOutputPath, bundle, forceAppendix, page, ruler, dl)
|
||||
childrenBoards, err := render(ctx, ms, compileDur, plugin, opts, inputPath, stepsOutputPath, bundle, forceAppendix, page, ruler, dl, ext)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -808,7 +834,7 @@ func render(ctx context.Context, ms *xmain.State, compileDur time.Duration, plug
|
|||
|
||||
if !diagram.IsFolderOnly {
|
||||
start := time.Now()
|
||||
out, err := _render(ctx, ms, plugin, opts, boardOutputPath, bundle, forceAppendix, page, ruler, diagram)
|
||||
out, err := _render(ctx, ms, plugin, opts, inputPath, boardOutputPath, bundle, forceAppendix, page, ruler, diagram, ext)
|
||||
if err != nil {
|
||||
return boards, err
|
||||
}
|
||||
|
|
@ -822,9 +848,9 @@ func render(ctx context.Context, ms *xmain.State, compileDur time.Duration, plug
|
|||
return boards, nil
|
||||
}
|
||||
|
||||
func renderSingle(ctx context.Context, ms *xmain.State, compileDur time.Duration, plugin d2plugin.Plugin, opts d2svg.RenderOpts, inputPath, outputPath string, bundle, forceAppendix bool, page playwright.Page, ruler *textmeasure.Ruler, diagram *d2target.Diagram) ([][]byte, error) {
|
||||
func renderSingle(ctx context.Context, ms *xmain.State, compileDur time.Duration, plugin d2plugin.Plugin, opts d2svg.RenderOpts, inputPath, outputPath string, bundle, forceAppendix bool, page playwright.Page, ruler *textmeasure.Ruler, diagram *d2target.Diagram, outputFormat exportExtension) ([][]byte, error) {
|
||||
start := time.Now()
|
||||
out, err := _render(ctx, ms, plugin, opts, outputPath, bundle, forceAppendix, page, ruler, diagram)
|
||||
out, err := _render(ctx, ms, plugin, opts, inputPath, outputPath, bundle, forceAppendix, page, ruler, diagram, outputFormat)
|
||||
if err != nil {
|
||||
return [][]byte{}, err
|
||||
}
|
||||
|
|
@ -835,49 +861,55 @@ func renderSingle(ctx context.Context, ms *xmain.State, compileDur time.Duration
|
|||
return [][]byte{out}, nil
|
||||
}
|
||||
|
||||
func _render(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opts d2svg.RenderOpts, outputPath string, bundle, forceAppendix bool, page playwright.Page, ruler *textmeasure.Ruler, diagram *d2target.Diagram) ([]byte, error) {
|
||||
toPNG := getExportExtension(outputPath) == PNG
|
||||
func _render(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opts d2svg.RenderOpts, inputPath, outputPath string, bundle, forceAppendix bool, page playwright.Page, ruler *textmeasure.Ruler, diagram *d2target.Diagram, outputFormat exportExtension) ([]byte, error) {
|
||||
toPNG := outputFormat == PNG
|
||||
|
||||
var scale *float64
|
||||
if opts.Scale != nil {
|
||||
scale = opts.Scale
|
||||
} else if toPNG {
|
||||
scale = go2.Pointer(1.)
|
||||
}
|
||||
svg, err := d2svg.Render(diagram, &d2svg.RenderOpts{
|
||||
renderOpts := &d2svg.RenderOpts{
|
||||
Pad: opts.Pad,
|
||||
Sketch: opts.Sketch,
|
||||
Center: opts.Center,
|
||||
MasterID: opts.MasterID,
|
||||
ThemeID: opts.ThemeID,
|
||||
DarkThemeID: opts.DarkThemeID,
|
||||
MasterID: opts.MasterID,
|
||||
ThemeOverrides: opts.ThemeOverrides,
|
||||
DarkThemeOverrides: opts.DarkThemeOverrides,
|
||||
NoXMLTag: opts.NoXMLTag,
|
||||
Salt: opts.Salt,
|
||||
Scale: scale,
|
||||
})
|
||||
}
|
||||
svg, err := d2svg.Render(diagram, renderOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
svg, err = plugin.PostProcess(ctx, svg)
|
||||
if err != nil {
|
||||
return svg, err
|
||||
if opts.MasterID == "" {
|
||||
svg, err = plugin.PostProcess(ctx, svg)
|
||||
if err != nil {
|
||||
return svg, err
|
||||
}
|
||||
}
|
||||
|
||||
cacheImages := ms.Env.Getenv("IMG_CACHE") == "1"
|
||||
l := simplelog.FromCmdLog(ms.Log)
|
||||
svg, bundleErr := imgbundler.BundleLocal(ctx, l, svg, cacheImages)
|
||||
svg, bundleErr := imgbundler.BundleLocal(ctx, l, inputPath, svg, cacheImages)
|
||||
if bundle {
|
||||
var bundleErr2 error
|
||||
svg, bundleErr2 = imgbundler.BundleRemote(ctx, l, svg, cacheImages)
|
||||
bundleErr = multierr.Combine(bundleErr, bundleErr2)
|
||||
}
|
||||
if forceAppendix && !toPNG {
|
||||
svg = appendix.Append(diagram, ruler, svg)
|
||||
svg = appendix.Append(diagram, renderOpts, ruler, svg)
|
||||
}
|
||||
|
||||
out := svg
|
||||
if toPNG {
|
||||
svg := appendix.Append(diagram, ruler, svg)
|
||||
svg := appendix.Append(diagram, renderOpts, ruler, svg)
|
||||
|
||||
if !bundle {
|
||||
var bundleErr2 error
|
||||
|
|
@ -904,7 +936,7 @@ func _render(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opts
|
|||
if err != nil {
|
||||
return svg, err
|
||||
}
|
||||
err = ms.WritePath(outputPath, out)
|
||||
err = Write(ms, outputPath, out)
|
||||
if err != nil {
|
||||
return svg, err
|
||||
}
|
||||
|
|
@ -915,7 +947,7 @@ func _render(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opts
|
|||
return svg, nil
|
||||
}
|
||||
|
||||
func renderPDF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opts d2svg.RenderOpts, outputPath string, page playwright.Page, ruler *textmeasure.Ruler, diagram *d2target.Diagram, doc *pdf.GoFPDF, boardPath []pdf.BoardTitle, pageMap map[string]int, includeNav bool) (svg []byte, err error) {
|
||||
func renderPDF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opts d2svg.RenderOpts, inputPath, outputPath string, page playwright.Page, ruler *textmeasure.Ruler, diagram *d2target.Diagram, doc *pdf.GoFPDF, boardPath []pdf.BoardTitle, pageMap map[string]int, includeNav bool) (svg []byte, err error) {
|
||||
var isRoot bool
|
||||
if doc == nil {
|
||||
doc = pdf.Init()
|
||||
|
|
@ -935,13 +967,17 @@ func renderPDF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opt
|
|||
scale = go2.Pointer(1.)
|
||||
}
|
||||
|
||||
svg, err = d2svg.Render(diagram, &d2svg.RenderOpts{
|
||||
Pad: opts.Pad,
|
||||
Sketch: opts.Sketch,
|
||||
Center: opts.Center,
|
||||
Scale: scale,
|
||||
ThemeID: opts.ThemeID,
|
||||
})
|
||||
renderOpts := &d2svg.RenderOpts{
|
||||
Pad: opts.Pad,
|
||||
Sketch: opts.Sketch,
|
||||
Center: opts.Center,
|
||||
Scale: scale,
|
||||
ThemeID: opts.ThemeID,
|
||||
DarkThemeID: opts.DarkThemeID,
|
||||
ThemeOverrides: opts.ThemeOverrides,
|
||||
DarkThemeOverrides: opts.DarkThemeOverrides,
|
||||
}
|
||||
svg, err = d2svg.Render(diagram, renderOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -953,13 +989,13 @@ func renderPDF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opt
|
|||
|
||||
cacheImages := ms.Env.Getenv("IMG_CACHE") == "1"
|
||||
l := simplelog.FromCmdLog(ms.Log)
|
||||
svg, bundleErr := imgbundler.BundleLocal(ctx, l, svg, cacheImages)
|
||||
svg, bundleErr := imgbundler.BundleLocal(ctx, l, inputPath, svg, cacheImages)
|
||||
svg, bundleErr2 := imgbundler.BundleRemote(ctx, l, svg, cacheImages)
|
||||
bundleErr = multierr.Combine(bundleErr, bundleErr2)
|
||||
if bundleErr != nil {
|
||||
return svg, bundleErr
|
||||
}
|
||||
svg = appendix.Append(diagram, ruler, svg)
|
||||
svg = appendix.Append(diagram, renderOpts, ruler, svg)
|
||||
|
||||
pngImg, err := ConvertSVG(ms, page, svg)
|
||||
if err != nil {
|
||||
|
|
@ -986,7 +1022,7 @@ func renderPDF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opt
|
|||
Name: dl.Root.Label,
|
||||
BoardID: strings.Join([]string{boardPath[len(boardPath)-1].BoardID, LAYERS, dl.Name}, "."),
|
||||
})
|
||||
_, err := renderPDF(ctx, ms, plugin, opts, "", page, ruler, dl, doc, path, pageMap, includeNav)
|
||||
_, err := renderPDF(ctx, ms, plugin, opts, inputPath, "", page, ruler, dl, doc, path, pageMap, includeNav)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -996,7 +1032,7 @@ func renderPDF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opt
|
|||
Name: dl.Root.Label,
|
||||
BoardID: strings.Join([]string{boardPath[len(boardPath)-1].BoardID, SCENARIOS, dl.Name}, "."),
|
||||
})
|
||||
_, err := renderPDF(ctx, ms, plugin, opts, "", page, ruler, dl, doc, path, pageMap, includeNav)
|
||||
_, err := renderPDF(ctx, ms, plugin, opts, inputPath, "", page, ruler, dl, doc, path, pageMap, includeNav)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -1006,7 +1042,7 @@ func renderPDF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opt
|
|||
Name: dl.Root.Label,
|
||||
BoardID: strings.Join([]string{boardPath[len(boardPath)-1].BoardID, STEPS, dl.Name}, "."),
|
||||
})
|
||||
_, err := renderPDF(ctx, ms, plugin, opts, "", page, ruler, dl, doc, path, pageMap, includeNav)
|
||||
_, err := renderPDF(ctx, ms, plugin, opts, inputPath, "", page, ruler, dl, doc, path, pageMap, includeNav)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -1022,7 +1058,7 @@ func renderPDF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opt
|
|||
return svg, nil
|
||||
}
|
||||
|
||||
func renderPPTX(ctx context.Context, ms *xmain.State, presentation *pptx.Presentation, plugin d2plugin.Plugin, opts d2svg.RenderOpts, ruler *textmeasure.Ruler, outputPath string, page playwright.Page, diagram *d2target.Diagram, boardPath []pptx.BoardTitle, boardIDToIndex map[string]int) ([]byte, error) {
|
||||
func renderPPTX(ctx context.Context, ms *xmain.State, presentation *pptx.Presentation, plugin d2plugin.Plugin, opts d2svg.RenderOpts, ruler *textmeasure.Ruler, inputPath, outputPath string, page playwright.Page, diagram *d2target.Diagram, boardPath []pptx.BoardTitle, boardIDToIndex map[string]int) ([]byte, error) {
|
||||
var svg []byte
|
||||
if !diagram.IsFolderOnly {
|
||||
// gofpdf will print the png img with a slight filter
|
||||
|
|
@ -1038,12 +1074,17 @@ func renderPPTX(ctx context.Context, ms *xmain.State, presentation *pptx.Present
|
|||
|
||||
var err error
|
||||
|
||||
svg, err = d2svg.Render(diagram, &d2svg.RenderOpts{
|
||||
Pad: opts.Pad,
|
||||
Sketch: opts.Sketch,
|
||||
Center: opts.Center,
|
||||
Scale: scale,
|
||||
})
|
||||
renderOpts := &d2svg.RenderOpts{
|
||||
Pad: opts.Pad,
|
||||
Sketch: opts.Sketch,
|
||||
Center: opts.Center,
|
||||
Scale: scale,
|
||||
ThemeID: opts.ThemeID,
|
||||
DarkThemeID: opts.DarkThemeID,
|
||||
ThemeOverrides: opts.ThemeOverrides,
|
||||
DarkThemeOverrides: opts.DarkThemeOverrides,
|
||||
}
|
||||
svg, err = d2svg.Render(diagram, renderOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -1055,14 +1096,14 @@ func renderPPTX(ctx context.Context, ms *xmain.State, presentation *pptx.Present
|
|||
|
||||
cacheImages := ms.Env.Getenv("IMG_CACHE") == "1"
|
||||
l := simplelog.FromCmdLog(ms.Log)
|
||||
svg, bundleErr := imgbundler.BundleLocal(ctx, l, svg, cacheImages)
|
||||
svg, bundleErr := imgbundler.BundleLocal(ctx, l, inputPath, svg, cacheImages)
|
||||
svg, bundleErr2 := imgbundler.BundleRemote(ctx, l, svg, cacheImages)
|
||||
bundleErr = multierr.Combine(bundleErr, bundleErr2)
|
||||
if bundleErr != nil {
|
||||
return nil, bundleErr
|
||||
}
|
||||
|
||||
svg = appendix.Append(diagram, ruler, svg)
|
||||
svg = appendix.Append(diagram, renderOpts, ruler, svg)
|
||||
|
||||
pngImg, err := ConvertSVG(ms, page, svg)
|
||||
if err != nil {
|
||||
|
|
@ -1120,7 +1161,7 @@ func renderPPTX(ctx context.Context, ms *xmain.State, presentation *pptx.Present
|
|||
BoardID: boardID,
|
||||
LinkToSlide: boardIDToIndex[boardID] + 1,
|
||||
})
|
||||
_, err := renderPPTX(ctx, ms, presentation, plugin, opts, ruler, "", page, dl, path, boardIDToIndex)
|
||||
_, err := renderPPTX(ctx, ms, presentation, plugin, opts, ruler, inputPath, "", page, dl, path, boardIDToIndex)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -1132,7 +1173,7 @@ func renderPPTX(ctx context.Context, ms *xmain.State, presentation *pptx.Present
|
|||
BoardID: boardID,
|
||||
LinkToSlide: boardIDToIndex[boardID] + 1,
|
||||
})
|
||||
_, err := renderPPTX(ctx, ms, presentation, plugin, opts, ruler, "", page, dl, path, boardIDToIndex)
|
||||
_, err := renderPPTX(ctx, ms, presentation, plugin, opts, ruler, inputPath, "", page, dl, path, boardIDToIndex)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -1144,7 +1185,7 @@ func renderPPTX(ctx context.Context, ms *xmain.State, presentation *pptx.Present
|
|||
BoardID: boardID,
|
||||
LinkToSlide: boardIDToIndex[boardID] + 1,
|
||||
})
|
||||
_, err := renderPPTX(ctx, ms, presentation, plugin, opts, ruler, "", page, dl, path, boardIDToIndex)
|
||||
_, err := renderPPTX(ctx, ms, presentation, plugin, opts, ruler, inputPath, "", page, dl, path, boardIDToIndex)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -1168,11 +1209,6 @@ func getFileName(path string) string {
|
|||
return strings.TrimSuffix(filepath.Base(path), ext)
|
||||
}
|
||||
|
||||
// TODO: remove after removing slog
|
||||
func DiscardSlog(ctx context.Context) context.Context {
|
||||
return ctxlog.With(ctx, slog.Make(sloghuman.Sink(io.Discard)))
|
||||
}
|
||||
|
||||
func populateLayoutOpts(ctx context.Context, ms *xmain.State, ps []d2plugin.Plugin) error {
|
||||
pluginFlags, err := d2plugin.ListPluginFlags(ctx, ps)
|
||||
if err != nil {
|
||||
|
|
@ -1276,7 +1312,7 @@ func buildBoardIDToIndex(diagram *d2target.Diagram, dictionary map[string]int, p
|
|||
return dictionary
|
||||
}
|
||||
|
||||
func renderPNGsForGIF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opts d2svg.RenderOpts, ruler *textmeasure.Ruler, page playwright.Page, diagram *d2target.Diagram) (svg []byte, pngs [][]byte, err error) {
|
||||
func renderPNGsForGIF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opts d2svg.RenderOpts, ruler *textmeasure.Ruler, page playwright.Page, inputPath string, diagram *d2target.Diagram) (svg []byte, pngs [][]byte, err error) {
|
||||
if !diagram.IsFolderOnly {
|
||||
|
||||
var scale *float64
|
||||
|
|
@ -1285,12 +1321,17 @@ func renderPNGsForGIF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plug
|
|||
} else {
|
||||
scale = go2.Pointer(1.)
|
||||
}
|
||||
svg, err = d2svg.Render(diagram, &d2svg.RenderOpts{
|
||||
Pad: opts.Pad,
|
||||
Sketch: opts.Sketch,
|
||||
Center: opts.Center,
|
||||
Scale: scale,
|
||||
})
|
||||
renderOpts := &d2svg.RenderOpts{
|
||||
Pad: opts.Pad,
|
||||
Sketch: opts.Sketch,
|
||||
Center: opts.Center,
|
||||
Scale: scale,
|
||||
ThemeID: opts.ThemeID,
|
||||
DarkThemeID: opts.DarkThemeID,
|
||||
ThemeOverrides: opts.ThemeOverrides,
|
||||
DarkThemeOverrides: opts.DarkThemeOverrides,
|
||||
}
|
||||
svg, err = d2svg.Render(diagram, renderOpts)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
|
@ -1302,14 +1343,14 @@ func renderPNGsForGIF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plug
|
|||
|
||||
cacheImages := ms.Env.Getenv("IMG_CACHE") == "1"
|
||||
l := simplelog.FromCmdLog(ms.Log)
|
||||
svg, bundleErr := imgbundler.BundleLocal(ctx, l, svg, cacheImages)
|
||||
svg, bundleErr := imgbundler.BundleLocal(ctx, l, inputPath, svg, cacheImages)
|
||||
svg, bundleErr2 := imgbundler.BundleRemote(ctx, l, svg, cacheImages)
|
||||
bundleErr = multierr.Combine(bundleErr, bundleErr2)
|
||||
if bundleErr != nil {
|
||||
return nil, nil, bundleErr
|
||||
}
|
||||
|
||||
svg = appendix.Append(diagram, ruler, svg)
|
||||
svg = appendix.Append(diagram, renderOpts, ruler, svg)
|
||||
|
||||
pngImg, err := ConvertSVG(ms, page, svg)
|
||||
if err != nil {
|
||||
|
|
@ -1319,21 +1360,21 @@ func renderPNGsForGIF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plug
|
|||
}
|
||||
|
||||
for _, dl := range diagram.Layers {
|
||||
_, layerPNGs, err := renderPNGsForGIF(ctx, ms, plugin, opts, ruler, page, dl)
|
||||
_, layerPNGs, err := renderPNGsForGIF(ctx, ms, plugin, opts, ruler, page, inputPath, dl)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
pngs = append(pngs, layerPNGs...)
|
||||
}
|
||||
for _, dl := range diagram.Scenarios {
|
||||
_, scenarioPNGs, err := renderPNGsForGIF(ctx, ms, plugin, opts, ruler, page, dl)
|
||||
_, scenarioPNGs, err := renderPNGsForGIF(ctx, ms, plugin, opts, ruler, page, inputPath, dl)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
pngs = append(pngs, scenarioPNGs...)
|
||||
}
|
||||
for _, dl := range diagram.Steps {
|
||||
_, stepsPNGs, err := renderPNGsForGIF(ctx, ms, plugin, opts, ruler, page, dl)
|
||||
_, stepsPNGs, err := renderPNGsForGIF(ctx, ms, plugin, opts, ruler, page, inputPath, dl)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
|
@ -1361,6 +1402,15 @@ func AnimatePNGs(ms *xmain.State, pngs [][]byte, animIntervalMs int) ([]byte, er
|
|||
return xgif.AnimatePNGs(pngs, animIntervalMs)
|
||||
}
|
||||
|
||||
func init() {
|
||||
ctxlog.Init()
|
||||
func Write(ms *xmain.State, path string, out []byte) error {
|
||||
err := ms.AtomicWritePath(path, out)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
ms.Log.Debug.Printf("atomic write failed: %s, trying non-atomic write", err.Error())
|
||||
return ms.WritePath(path, out)
|
||||
}
|
||||
|
||||
func init() {
|
||||
log.Init()
|
||||
}
|
||||
|
|
|
|||
75
d2cli/play.go
Normal file
75
d2cli/play.go
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
package d2cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"oss.terrastruct.com/d2/lib/urlenc"
|
||||
"oss.terrastruct.com/util-go/xbrowser"
|
||||
"oss.terrastruct.com/util-go/xmain"
|
||||
)
|
||||
|
||||
func playCmd(ctx context.Context, ms *xmain.State) error {
|
||||
if len(ms.Opts.Flags.Args()) != 2 {
|
||||
return xmain.UsageErrorf("play must be passed one argument: either a filepath or '-' for stdin")
|
||||
}
|
||||
filepath := ms.Opts.Flags.Args()[1]
|
||||
|
||||
theme, err := ms.Opts.Flags.GetInt64("theme")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sketch, err := ms.Opts.Flags.GetBool("sketch")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var sketchNumber int
|
||||
if sketch {
|
||||
sketchNumber = 1
|
||||
} else {
|
||||
sketchNumber = 0
|
||||
}
|
||||
|
||||
fileRaw, err := readInput(filepath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
encoded, err := urlenc.Encode(fileRaw)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("https://play.d2lang.com/?script=%s&sketch=%d&theme=%d&", encoded, sketchNumber, theme)
|
||||
openBrowser(ctx, ms, url)
|
||||
return nil
|
||||
}
|
||||
|
||||
func readInput(filepath string) (string, error) {
|
||||
if filepath == "-" {
|
||||
data, err := io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error reading from stdin: %w", err)
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(filepath)
|
||||
if err != nil {
|
||||
return "", xmain.UsageErrorf(err.Error())
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
func openBrowser(ctx context.Context, ms *xmain.State, url string) {
|
||||
ms.Log.Info.Printf("opening playground: %s", url)
|
||||
|
||||
err := xbrowser.Open(ctx, ms.Env, url)
|
||||
if err != nil {
|
||||
ms.Log.Warn.Printf("failed to open browser to %v: %v", url, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -27,7 +27,7 @@ function init(reconnectDelay) {
|
|||
const parsedXML = new DOMParser().parseFromString(msg.svg, "text/xml");
|
||||
d2SVG.replaceChildren(parsedXML.documentElement);
|
||||
changeFavicon("/static/favicon.ico");
|
||||
const svgEl = d2SVG.querySelector("#d2-svg");
|
||||
const svgEl = d2SVG.querySelector(".d2-svg");
|
||||
// just use inner SVG in watch mode
|
||||
svgEl.parentElement.replaceWith(svgEl);
|
||||
let width = parseInt(svgEl.getAttribute("width"), 10);
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ type watcherOpts struct {
|
|||
forceAppendix bool
|
||||
pw png.Playwright
|
||||
fontFamily *d2fonts.FontFamily
|
||||
outputFormat exportExtension
|
||||
}
|
||||
|
||||
type watcher struct {
|
||||
|
|
@ -430,7 +431,7 @@ func (w *watcher) compileLoop(ctx context.Context) error {
|
|||
if w.boardPath != "" {
|
||||
boardPath = strings.Split(w.boardPath, string(os.PathSeparator))
|
||||
}
|
||||
svg, _, err := compile(ctx, w.ms, w.plugins, &fs, w.layout, w.renderOpts, w.fontFamily, w.animateInterval, w.inputPath, w.outputPath, boardPath, false, w.bundle, w.forceAppendix, w.pw.Page)
|
||||
svg, _, err := compile(ctx, w.ms, w.plugins, &fs, w.layout, w.renderOpts, w.fontFamily, w.animateInterval, w.inputPath, w.outputPath, boardPath, false, w.bundle, w.forceAppendix, w.pw.Page, w.outputFormat)
|
||||
w.boardpathMu.Unlock()
|
||||
errs := ""
|
||||
if err != nil {
|
||||
|
|
@ -520,6 +521,11 @@ func (w *watcher) handleRoot(hw http.ResponseWriter, r *http.Request) {
|
|||
if idx := strings.LastIndexByte(boardPath, '.'); idx != -1 {
|
||||
boardPath = boardPath[:idx]
|
||||
}
|
||||
// if path is "/index", we just want "/"
|
||||
boardPath = strings.TrimSuffix(boardPath, "/index")
|
||||
if boardPath == "index" {
|
||||
boardPath = ""
|
||||
}
|
||||
recompile := false
|
||||
if boardPath != w.boardPath {
|
||||
w.boardPath = boardPath
|
||||
|
|
|
|||
|
|
@ -3,9 +3,11 @@ package d2compiler
|
|||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"html"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
|
|
@ -69,6 +71,7 @@ func compileIR(ast *d2ast.Map, m *d2ir.Map) (*d2graph.Graph, error) {
|
|||
|
||||
g := d2graph.NewGraph()
|
||||
g.AST = ast
|
||||
g.BaseAST = ast
|
||||
c.compileBoard(g, m)
|
||||
if len(c.err.Errors) > 0 {
|
||||
return nil, c.err
|
||||
|
|
@ -107,24 +110,30 @@ func (c *compiler) compileBoard(g *d2graph.Graph, ir *d2ir.Map) *d2graph.Graph {
|
|||
}
|
||||
|
||||
func (c *compiler) compileBoardsField(g *d2graph.Graph, ir *d2ir.Map, fieldName string) {
|
||||
layers := ir.GetField(fieldName)
|
||||
if layers.Map() == nil {
|
||||
boards := ir.GetField(d2ast.FlatUnquotedString(fieldName))
|
||||
if boards.Map() == nil {
|
||||
return
|
||||
}
|
||||
for _, f := range layers.Map().Fields {
|
||||
for _, f := range boards.Map().Fields {
|
||||
m := f.Map()
|
||||
if f.Map() == nil {
|
||||
continue
|
||||
m = &d2ir.Map{}
|
||||
}
|
||||
if g.GetBoard(f.Name) != nil {
|
||||
c.errorf(f.References[0].AST(), "board name %v already used by another board", f.Name)
|
||||
if g.GetBoard(f.Name.ScalarString()) != nil {
|
||||
c.errorf(f.References[0].AST(), "board name %v already used by another board", f.Name.ScalarString())
|
||||
continue
|
||||
}
|
||||
g2 := d2graph.NewGraph()
|
||||
g2.Parent = g
|
||||
g2.AST = f.Map().AST().(*d2ast.Map)
|
||||
g2.BaseAST = findFieldAST(g.AST, f)
|
||||
c.compileBoard(g2, f.Map())
|
||||
g2.Name = f.Name
|
||||
g2.AST = m.AST().(*d2ast.Map)
|
||||
if g.BaseAST != nil {
|
||||
g2.BaseAST = findFieldAST(g.BaseAST, f)
|
||||
}
|
||||
c.compileBoard(g2, m)
|
||||
if f.Primary() != nil {
|
||||
c.compileLabel(&g2.Root.Attributes, f)
|
||||
}
|
||||
g2.Name = f.Name.ScalarString()
|
||||
switch fieldName {
|
||||
case "layers":
|
||||
g.Layers = append(g.Layers, g2)
|
||||
|
|
@ -138,9 +147,9 @@ func (c *compiler) compileBoardsField(g *d2graph.Graph, ir *d2ir.Map, fieldName
|
|||
|
||||
func findFieldAST(ast *d2ast.Map, f *d2ir.Field) *d2ast.Map {
|
||||
path := []string{}
|
||||
var curr *d2ir.Field = f
|
||||
curr := f
|
||||
for {
|
||||
path = append([]string{curr.Name}, path...)
|
||||
path = append([]string{curr.Name.ScalarString()}, path...)
|
||||
boardKind := d2ir.NodeBoardKind(curr)
|
||||
if boardKind == "" {
|
||||
break
|
||||
|
|
@ -215,7 +224,7 @@ func (c *compiler) errorf(n d2ast.Node, f string, v ...interface{}) {
|
|||
}
|
||||
|
||||
func (c *compiler) compileMap(obj *d2graph.Object, m *d2ir.Map) {
|
||||
class := m.GetField("class")
|
||||
class := m.GetField(d2ast.FlatUnquotedString("class"))
|
||||
if class != nil {
|
||||
var classNames []string
|
||||
if class.Primary() != nil {
|
||||
|
|
@ -230,8 +239,6 @@ func (c *compiler) compileMap(obj *d2graph.Object, m *d2ir.Map) {
|
|||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
c.errorf(class.LastRef().AST(), "class missing value")
|
||||
}
|
||||
|
||||
for _, className := range classNames {
|
||||
|
|
@ -256,7 +263,7 @@ func (c *compiler) compileMap(obj *d2graph.Object, m *d2ir.Map) {
|
|||
}
|
||||
}
|
||||
}
|
||||
shape := m.GetField("shape")
|
||||
shape := m.GetField(d2ast.FlatUnquotedString("shape"))
|
||||
if shape != nil {
|
||||
if shape.Composite != nil {
|
||||
c.errorf(shape.LastPrimaryKey(), "reserved field shape does not accept composite")
|
||||
|
|
@ -265,10 +272,10 @@ func (c *compiler) compileMap(obj *d2graph.Object, m *d2ir.Map) {
|
|||
}
|
||||
}
|
||||
for _, f := range m.Fields {
|
||||
if f.Name == "shape" {
|
||||
if f.Name.ScalarString() == "shape" && f.Name.IsUnquoted() {
|
||||
continue
|
||||
}
|
||||
if _, ok := d2graph.BoardKeywords[f.Name]; ok {
|
||||
if _, ok := d2ast.BoardKeywords[f.Name.ScalarString()]; ok && f.Name.IsUnquoted() {
|
||||
continue
|
||||
}
|
||||
c.compileField(obj, f)
|
||||
|
|
@ -289,14 +296,15 @@ func (c *compiler) compileMap(obj *d2graph.Object, m *d2ir.Map) {
|
|||
}
|
||||
|
||||
func (c *compiler) compileField(obj *d2graph.Object, f *d2ir.Field) {
|
||||
keyword := strings.ToLower(f.Name)
|
||||
_, isStyleReserved := d2graph.StyleKeywords[keyword]
|
||||
if isStyleReserved {
|
||||
c.errorf(f.LastRef().AST(), "%v must be style.%v", f.Name, f.Name)
|
||||
keyword := strings.ToLower(f.Name.ScalarString())
|
||||
_, isStyleReserved := d2ast.StyleKeywords[keyword]
|
||||
if isStyleReserved && f.Name.IsUnquoted() {
|
||||
c.errorf(f.LastRef().AST(), "%v must be style.%v", f.Name.ScalarString(), f.Name.ScalarString())
|
||||
return
|
||||
}
|
||||
_, isReserved := d2graph.SimpleReservedKeywords[keyword]
|
||||
if f.Name == "classes" {
|
||||
_, isReserved := d2ast.SimpleReservedKeywords[keyword]
|
||||
isReserved = isReserved && f.Name.IsUnquoted()
|
||||
if f.Name.ScalarString() == "classes" && f.Name.IsUnquoted() {
|
||||
if f.Map() != nil {
|
||||
if len(f.Map().Edges) > 0 {
|
||||
c.errorf(f.Map().Edges[0].LastRef().AST(), "classes cannot contain an edge")
|
||||
|
|
@ -306,34 +314,31 @@ func (c *compiler) compileField(obj *d2graph.Object, f *d2ir.Field) {
|
|||
continue
|
||||
}
|
||||
for _, cf := range classesField.Map().Fields {
|
||||
if _, ok := d2graph.ReservedKeywords[cf.Name]; !ok {
|
||||
c.errorf(cf.LastRef().AST(), "%s is an invalid class field, must be reserved keyword", cf.Name)
|
||||
if _, ok := d2ast.ReservedKeywords[cf.Name.ScalarString()]; !(ok && f.Name.IsUnquoted()) {
|
||||
c.errorf(cf.LastRef().AST(), "%s is an invalid class field, must be reserved keyword", cf.Name.ScalarString())
|
||||
}
|
||||
if cf.Name == "class" {
|
||||
if cf.Name.ScalarString() == "class" && cf.Name.IsUnquoted() {
|
||||
c.errorf(cf.LastRef().AST(), `"class" cannot appear within "classes"`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
} else if f.Name == "vars" {
|
||||
} else if f.Name.ScalarString() == "vars" && f.Name.IsUnquoted() {
|
||||
return
|
||||
} else if f.Name == "source-arrowhead" || f.Name == "target-arrowhead" {
|
||||
c.errorf(f.LastRef().AST(), `%#v can only be used on connections`, f.Name)
|
||||
} else if (f.Name.ScalarString() == "source-arrowhead" || f.Name.ScalarString() == "target-arrowhead") && f.Name.IsUnquoted() {
|
||||
c.errorf(f.LastRef().AST(), `%#v can only be used on connections`, f.Name.ScalarString())
|
||||
return
|
||||
|
||||
} else if isReserved {
|
||||
c.compileReserved(&obj.Attributes, f)
|
||||
return
|
||||
} else if f.Name == "style" {
|
||||
} else if f.Name.ScalarString() == "style" && f.Name.IsUnquoted() {
|
||||
if f.Map() == nil || len(f.Map().Fields) == 0 {
|
||||
c.errorf(f.LastRef().AST(), `"style" expected to be set to a map of key-values, or contain an additional keyword like "style.opacity: 0.4"`)
|
||||
return
|
||||
}
|
||||
c.compileStyle(&obj.Attributes, f.Map())
|
||||
if obj.Style.Animated != nil {
|
||||
c.errorf(obj.Style.Animated.MapKey, `key "animated" can only be applied to edges`)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -348,7 +353,8 @@ func (c *compiler) compileField(obj *d2graph.Object, f *d2ir.Field) {
|
|||
}
|
||||
}
|
||||
|
||||
obj = obj.EnsureChild(d2graphIDA([]string{f.Name}))
|
||||
parent := obj
|
||||
obj = obj.EnsureChild(([]d2ast.String{f.Name}))
|
||||
if f.Primary() != nil {
|
||||
c.compileLabel(&obj.Attributes, f)
|
||||
}
|
||||
|
|
@ -373,9 +379,11 @@ func (c *compiler) compileField(obj *d2graph.Object, f *d2ir.Field) {
|
|||
MapKeyEdgeIndex: fr.Context_.EdgeIndex(),
|
||||
Scope: fr.Context_.Scope,
|
||||
ScopeAST: fr.Context_.ScopeAST,
|
||||
ScopeObj: parent,
|
||||
IsVar: d2ir.IsVar(fr.Context_.ScopeMap),
|
||||
}
|
||||
if fr.Context_.ScopeMap != nil && !d2ir.IsVar(fr.Context_.ScopeMap) {
|
||||
scopeObjIDA := d2graphIDA(d2ir.BoardIDA(fr.Context_.ScopeMap))
|
||||
scopeObjIDA := d2ir.BoardIDA(fr.Context_.ScopeMap)
|
||||
r.ScopeObj = obj.Graph.Root.EnsureChild(scopeObjIDA)
|
||||
}
|
||||
obj.References = append(obj.References, r)
|
||||
|
|
@ -428,7 +436,7 @@ func (c *compiler) compilePosition(attrs *d2graph.Attributes, f *d2ir.Field) {
|
|||
name := f.Name
|
||||
if f.Map() != nil {
|
||||
for _, f := range f.Map().Fields {
|
||||
if f.Name == "near" {
|
||||
if f.Name.ScalarString() == "near" && f.Name.IsUnquoted() {
|
||||
if f.Primary() == nil {
|
||||
c.errorf(f.LastPrimaryKey(), `invalid "near" field`)
|
||||
} else {
|
||||
|
|
@ -437,10 +445,10 @@ func (c *compiler) compilePosition(attrs *d2graph.Attributes, f *d2ir.Field) {
|
|||
case *d2ast.Null:
|
||||
attrs.LabelPosition = nil
|
||||
default:
|
||||
if _, ok := d2graph.LabelPositions[scalar.ScalarString()]; !ok {
|
||||
if _, ok := d2ast.LabelPositions[scalar.ScalarString()]; !ok {
|
||||
c.errorf(f.LastPrimaryKey(), `invalid "near" field`)
|
||||
} else {
|
||||
switch name {
|
||||
switch name.ScalarString() {
|
||||
case "label":
|
||||
attrs.LabelPosition = &d2graph.Scalar{}
|
||||
attrs.LabelPosition.Value = scalar.ScalarString()
|
||||
|
|
@ -455,7 +463,7 @@ func (c *compiler) compilePosition(attrs *d2graph.Attributes, f *d2ir.Field) {
|
|||
}
|
||||
} else {
|
||||
if f.LastPrimaryKey() != nil {
|
||||
c.errorf(f.LastPrimaryKey(), `unexpected field %s`, f.Name)
|
||||
c.errorf(f.LastPrimaryKey(), `unexpected field %s`, f.Name.ScalarString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -468,7 +476,7 @@ func (c *compiler) compilePosition(attrs *d2graph.Attributes, f *d2ir.Field) {
|
|||
func (c *compiler) compileReserved(attrs *d2graph.Attributes, f *d2ir.Field) {
|
||||
if f.Primary() == nil {
|
||||
if f.Composite != nil {
|
||||
switch f.Name {
|
||||
switch f.Name.ScalarString() {
|
||||
case "class":
|
||||
if arr, ok := f.Composite.(*d2ir.Array); ok {
|
||||
for _, class := range arr.Values {
|
||||
|
|
@ -493,13 +501,15 @@ func (c *compiler) compileReserved(attrs *d2graph.Attributes, f *d2ir.Field) {
|
|||
case "label", "icon":
|
||||
c.compilePosition(attrs, f)
|
||||
default:
|
||||
c.errorf(f.LastPrimaryKey(), "reserved field %v does not accept composite", f.Name)
|
||||
c.errorf(f.LastPrimaryKey(), "reserved field %v does not accept composite", f.Name.ScalarString())
|
||||
}
|
||||
} else {
|
||||
c.errorf(f.LastRef().AST(), `reserved field "%v" must have a value`, f.Name.ScalarString())
|
||||
}
|
||||
return
|
||||
}
|
||||
scalar := f.Primary().Value
|
||||
switch f.Name {
|
||||
switch f.Name.ScalarString() {
|
||||
case "label":
|
||||
c.compileLabel(attrs, f)
|
||||
c.compilePosition(attrs, f)
|
||||
|
|
@ -668,6 +678,13 @@ func (c *compiler) compileReserved(attrs *d2graph.Attributes, f *d2ir.Field) {
|
|||
case "classes":
|
||||
}
|
||||
|
||||
if attrs.Link != nil && attrs.Label.Value != "" {
|
||||
u, err := url.ParseRequestURI(attrs.Label.Value)
|
||||
if err == nil && u.Host != "" {
|
||||
c.errorf(scalar, "Label cannot be set to URL when link is also set (for security)")
|
||||
}
|
||||
}
|
||||
|
||||
if attrs.Link != nil && attrs.Tooltip != nil {
|
||||
u, err := url.ParseRequestURI(attrs.Tooltip.Value)
|
||||
if err == nil && u.Host != "" {
|
||||
|
|
@ -683,8 +700,8 @@ func (c *compiler) compileStyle(attrs *d2graph.Attributes, m *d2ir.Map) {
|
|||
}
|
||||
|
||||
func (c *compiler) compileStyleField(attrs *d2graph.Attributes, f *d2ir.Field) {
|
||||
if _, ok := d2graph.StyleKeywords[strings.ToLower(f.Name)]; !ok {
|
||||
c.errorf(f.LastRef().AST(), `invalid style keyword: "%s"`, f.Name)
|
||||
if _, ok := d2ast.StyleKeywords[strings.ToLower(f.Name.ScalarString())]; !(ok && f.Name.IsUnquoted()) {
|
||||
c.errorf(f.LastRef().AST(), `invalid style keyword: "%s"`, f.Name.ScalarString())
|
||||
return
|
||||
}
|
||||
if f.Primary() == nil {
|
||||
|
|
@ -692,7 +709,7 @@ func (c *compiler) compileStyleField(attrs *d2graph.Attributes, f *d2ir.Field) {
|
|||
}
|
||||
compileStyleFieldInit(attrs, f)
|
||||
scalar := f.Primary().Value
|
||||
err := attrs.Style.Apply(f.Name, scalar.ScalarString())
|
||||
err := attrs.Style.Apply(f.Name.ScalarString(), scalar.ScalarString())
|
||||
if err != nil {
|
||||
c.errorf(scalar, err.Error())
|
||||
return
|
||||
|
|
@ -700,7 +717,7 @@ func (c *compiler) compileStyleField(attrs *d2graph.Attributes, f *d2ir.Field) {
|
|||
}
|
||||
|
||||
func compileStyleFieldInit(attrs *d2graph.Attributes, f *d2ir.Field) {
|
||||
switch f.Name {
|
||||
switch f.Name.ScalarString() {
|
||||
case "opacity":
|
||||
attrs.Style.Opacity = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
||||
case "stroke":
|
||||
|
|
@ -753,7 +770,7 @@ func compileStyleFieldInit(attrs *d2graph.Attributes, f *d2ir.Field) {
|
|||
}
|
||||
|
||||
func (c *compiler) compileEdge(obj *d2graph.Object, e *d2ir.Edge) {
|
||||
edge, err := obj.Connect(d2graphIDA(e.ID.SrcPath), d2graphIDA(e.ID.DstPath), e.ID.SrcArrow, e.ID.DstArrow, "")
|
||||
edge, err := obj.Connect(e.ID.SrcPath, e.ID.DstPath, e.ID.SrcArrow, e.ID.DstArrow, "")
|
||||
if err != nil {
|
||||
c.errorf(e.References[0].AST(), err.Error())
|
||||
return
|
||||
|
|
@ -774,9 +791,10 @@ func (c *compiler) compileEdge(obj *d2graph.Object, e *d2ir.Edge) {
|
|||
MapKeyEdgeIndex: er.Context_.EdgeIndex(),
|
||||
Scope: er.Context_.Scope,
|
||||
ScopeAST: er.Context_.ScopeAST,
|
||||
ScopeObj: obj,
|
||||
}
|
||||
if er.Context_.ScopeMap != nil && !d2ir.IsVar(er.Context_.ScopeMap) {
|
||||
scopeObjIDA := d2graphIDA(d2ir.BoardIDA(er.Context_.ScopeMap))
|
||||
scopeObjIDA := d2ir.BoardIDA(er.Context_.ScopeMap)
|
||||
r.ScopeObj = edge.Src.Graph.Root.EnsureChild(scopeObjIDA)
|
||||
}
|
||||
edge.References = append(edge.References, r)
|
||||
|
|
@ -784,7 +802,7 @@ func (c *compiler) compileEdge(obj *d2graph.Object, e *d2ir.Edge) {
|
|||
}
|
||||
|
||||
func (c *compiler) compileEdgeMap(edge *d2graph.Edge, m *d2ir.Map) {
|
||||
class := m.GetField("class")
|
||||
class := m.GetField(d2ast.FlatUnquotedString("class"))
|
||||
if class != nil {
|
||||
var classNames []string
|
||||
if class.Primary() != nil {
|
||||
|
|
@ -799,8 +817,6 @@ func (c *compiler) compileEdgeMap(edge *d2graph.Edge, m *d2ir.Map) {
|
|||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
c.errorf(class.LastRef().AST(), "class missing value")
|
||||
}
|
||||
|
||||
for _, className := range classNames {
|
||||
|
|
@ -811,8 +827,8 @@ func (c *compiler) compileEdgeMap(edge *d2graph.Edge, m *d2ir.Map) {
|
|||
}
|
||||
}
|
||||
for _, f := range m.Fields {
|
||||
_, ok := d2graph.ReservedKeywords[f.Name]
|
||||
if !ok {
|
||||
_, ok := d2ast.ReservedKeywords[f.Name.ScalarString()]
|
||||
if !(ok && f.Name.IsUnquoted()) {
|
||||
c.errorf(f.References[0].AST(), `edge map keys must be reserved keywords`)
|
||||
continue
|
||||
}
|
||||
|
|
@ -821,17 +837,18 @@ func (c *compiler) compileEdgeMap(edge *d2graph.Edge, m *d2ir.Map) {
|
|||
}
|
||||
|
||||
func (c *compiler) compileEdgeField(edge *d2graph.Edge, f *d2ir.Field) {
|
||||
keyword := strings.ToLower(f.Name)
|
||||
_, isStyleReserved := d2graph.StyleKeywords[keyword]
|
||||
keyword := strings.ToLower(f.Name.ScalarString())
|
||||
_, isStyleReserved := d2ast.StyleKeywords[keyword]
|
||||
isStyleReserved = isStyleReserved && f.Name.IsUnquoted()
|
||||
if isStyleReserved {
|
||||
c.errorf(f.LastRef().AST(), "%v must be style.%v", f.Name, f.Name)
|
||||
c.errorf(f.LastRef().AST(), "%v must be style.%v", f.Name.ScalarString(), f.Name.ScalarString())
|
||||
return
|
||||
}
|
||||
_, isReserved := d2graph.SimpleReservedKeywords[keyword]
|
||||
_, isReserved := d2ast.SimpleReservedKeywords[keyword]
|
||||
if isReserved {
|
||||
c.compileReserved(&edge.Attributes, f)
|
||||
return
|
||||
} else if f.Name == "style" {
|
||||
} else if f.Name.ScalarString() == "style" {
|
||||
if f.Map() == nil {
|
||||
return
|
||||
}
|
||||
|
|
@ -839,14 +856,14 @@ func (c *compiler) compileEdgeField(edge *d2graph.Edge, f *d2ir.Field) {
|
|||
return
|
||||
}
|
||||
|
||||
if f.Name == "source-arrowhead" || f.Name == "target-arrowhead" {
|
||||
if (f.Name.ScalarString() == "source-arrowhead" || f.Name.ScalarString() == "target-arrowhead") && f.Name.IsUnquoted() {
|
||||
c.compileArrowheads(edge, f)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *compiler) compileArrowheads(edge *d2graph.Edge, f *d2ir.Field) {
|
||||
var attrs *d2graph.Attributes
|
||||
if f.Name == "source-arrowhead" {
|
||||
if f.Name.ScalarString() == "source-arrowhead" {
|
||||
if edge.SrcArrowhead == nil {
|
||||
edge.SrcArrowhead = &d2graph.Attributes{}
|
||||
}
|
||||
|
|
@ -864,12 +881,13 @@ func (c *compiler) compileArrowheads(edge *d2graph.Edge, f *d2ir.Field) {
|
|||
|
||||
if f.Map() != nil {
|
||||
for _, f2 := range f.Map().Fields {
|
||||
keyword := strings.ToLower(f2.Name)
|
||||
_, isReserved := d2graph.SimpleReservedKeywords[keyword]
|
||||
keyword := strings.ToLower(f2.Name.ScalarString())
|
||||
_, isReserved := d2ast.SimpleReservedKeywords[keyword]
|
||||
isReserved = isReserved && f2.Name.IsUnquoted()
|
||||
if isReserved {
|
||||
c.compileReserved(attrs, f2)
|
||||
continue
|
||||
} else if f2.Name == "style" {
|
||||
} else if f2.Name.ScalarString() == "style" && f2.Name.IsUnquoted() {
|
||||
if f2.Map() == nil {
|
||||
continue
|
||||
}
|
||||
|
|
@ -982,7 +1000,7 @@ func (c *compiler) compileSQLTable(obj *d2graph.Object) {
|
|||
|
||||
func (c *compiler) validateKeys(obj *d2graph.Object, m *d2ir.Map) {
|
||||
for _, f := range m.Fields {
|
||||
if _, ok := d2graph.BoardKeywords[f.Name]; ok {
|
||||
if _, ok := d2ast.BoardKeywords[f.Name.ScalarString()]; ok && f.Name.IsUnquoted() {
|
||||
continue
|
||||
}
|
||||
c.validateKey(obj, f)
|
||||
|
|
@ -990,8 +1008,9 @@ func (c *compiler) validateKeys(obj *d2graph.Object, m *d2ir.Map) {
|
|||
}
|
||||
|
||||
func (c *compiler) validateKey(obj *d2graph.Object, f *d2ir.Field) {
|
||||
keyword := strings.ToLower(f.Name)
|
||||
_, isReserved := d2graph.ReservedKeywords[keyword]
|
||||
keyword := strings.ToLower(f.Name.ScalarString())
|
||||
_, isReserved := d2ast.ReservedKeywords[keyword]
|
||||
isReserved = isReserved && f.Name.IsUnquoted()
|
||||
if isReserved {
|
||||
switch obj.Shape.Value {
|
||||
case d2target.ShapeCircle, d2target.ShapeSquare:
|
||||
|
|
@ -1001,7 +1020,7 @@ func (c *compiler) validateKey(obj *d2graph.Object, f *d2ir.Field) {
|
|||
}
|
||||
}
|
||||
|
||||
switch f.Name {
|
||||
switch f.Name.ScalarString() {
|
||||
case "style":
|
||||
if obj.Style.ThreeDee != nil {
|
||||
if !strings.EqualFold(obj.Shape.Value, d2target.ShapeSquare) && !strings.EqualFold(obj.Shape.Value, d2target.ShapeRectangle) && !strings.EqualFold(obj.Shape.Value, d2target.ShapeHexagon) {
|
||||
|
|
@ -1031,12 +1050,12 @@ func (c *compiler) validateKey(obj *d2graph.Object, f *d2ir.Field) {
|
|||
return
|
||||
}
|
||||
|
||||
if strings.EqualFold(obj.Shape.Value, d2target.ShapeImage) {
|
||||
if strings.EqualFold(obj.Shape.Value, d2target.ShapeImage) && obj.OuterSequenceDiagram() == nil {
|
||||
c.errorf(f.LastRef().AST(), "image shapes cannot have children.")
|
||||
return
|
||||
}
|
||||
|
||||
obj, ok := obj.HasChild([]string{f.Name})
|
||||
obj, ok := obj.HasChild([]string{f.Name.ScalarString()})
|
||||
if ok && f.Map() != nil {
|
||||
c.validateKeys(obj, f.Map())
|
||||
}
|
||||
|
|
@ -1044,16 +1063,18 @@ func (c *compiler) validateKey(obj *d2graph.Object, f *d2ir.Field) {
|
|||
|
||||
func (c *compiler) validateLabels(g *d2graph.Graph) {
|
||||
for _, obj := range g.Objects {
|
||||
if !strings.EqualFold(obj.Shape.Value, d2target.ShapeText) {
|
||||
continue
|
||||
}
|
||||
if obj.Attributes.Language != "" {
|
||||
// blockstrings have already been validated
|
||||
continue
|
||||
}
|
||||
if strings.TrimSpace(obj.Label.Value) == "" {
|
||||
c.errorf(obj.Label.MapKey, "shape text must have a non-empty label")
|
||||
continue
|
||||
if strings.EqualFold(obj.Shape.Value, d2target.ShapeText) {
|
||||
if obj.Attributes.Language != "" {
|
||||
// blockstrings have already been validated
|
||||
continue
|
||||
}
|
||||
if strings.TrimSpace(obj.Label.Value) == "" {
|
||||
c.errorf(obj.Label.MapKey, "shape text must have a non-empty label")
|
||||
}
|
||||
} else if strings.EqualFold(obj.Shape.Value, d2target.ShapeSQLTable) {
|
||||
if strings.Contains(obj.Label.Value, "\n") {
|
||||
c.errorf(obj.Label.MapKey, "shape sql_table cannot have newlines in label")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1062,7 +1083,7 @@ func (c *compiler) validateNear(g *d2graph.Graph) {
|
|||
for _, obj := range g.Objects {
|
||||
if obj.NearKey != nil {
|
||||
nearObj, isKey := g.Root.HasChild(d2graph.Key(obj.NearKey))
|
||||
_, isConst := d2graph.NearConstants[d2graph.Key(obj.NearKey)[0]]
|
||||
_, isConst := d2ast.NearConstants[d2graph.Key(obj.NearKey)[0]]
|
||||
if isKey {
|
||||
// Doesn't make sense to set near to an ancestor or descendant
|
||||
nearIsAncestor := false
|
||||
|
|
@ -1092,7 +1113,7 @@ func (c *compiler) validateNear(g *d2graph.Graph) {
|
|||
continue
|
||||
}
|
||||
if nearObj.NearKey != nil {
|
||||
_, nearObjNearIsConst := d2graph.NearConstants[d2graph.Key(nearObj.NearKey)[0]]
|
||||
_, nearObjNearIsConst := d2ast.NearConstants[d2graph.Key(nearObj.NearKey)[0]]
|
||||
if nearObjNearIsConst {
|
||||
c.errorf(obj.NearKey, "near keys cannot be set to an object with a constant near key")
|
||||
continue
|
||||
|
|
@ -1112,7 +1133,7 @@ func (c *compiler) validateNear(g *d2graph.Graph) {
|
|||
continue
|
||||
}
|
||||
} else {
|
||||
c.errorf(obj.NearKey, "near key %#v must be the absolute path to a shape or one of the following constants: %s", d2format.Format(obj.NearKey), strings.Join(d2graph.NearConstantsArray, ", "))
|
||||
c.errorf(obj.NearKey, "near key %#v must be the absolute path to a shape or one of the following constants: %s", d2format.Format(obj.NearKey), strings.Join(d2ast.NearConstantsArray, ", "))
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
|
@ -1195,12 +1216,24 @@ func (c *compiler) validateBoardLinks(g *d2graph.Graph) {
|
|||
continue
|
||||
}
|
||||
|
||||
u, err := url.Parse(html.UnescapeString(obj.Link.Value))
|
||||
isRemote := err == nil && u.Scheme != ""
|
||||
if isRemote {
|
||||
continue
|
||||
}
|
||||
|
||||
if linkKey.Path[0].Unbox().ScalarString() != "root" {
|
||||
obj.Link = nil
|
||||
continue
|
||||
}
|
||||
|
||||
if !hasBoard(g.RootBoard(), linkKey.IDA()) {
|
||||
c.errorf(obj.Link.MapKey, "linked board not found")
|
||||
obj.Link = nil
|
||||
continue
|
||||
}
|
||||
|
||||
if slices.Equal(linkKey.StringIDA(), obj.Graph.IDA()) {
|
||||
obj.Link = nil
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
|
@ -1215,34 +1248,34 @@ func (c *compiler) validateBoardLinks(g *d2graph.Graph) {
|
|||
}
|
||||
}
|
||||
|
||||
func hasBoard(root *d2graph.Graph, ida []string) bool {
|
||||
func hasBoard(root *d2graph.Graph, ida []d2ast.String) bool {
|
||||
if len(ida) == 0 {
|
||||
return true
|
||||
}
|
||||
if ida[0] == "root" {
|
||||
if ida[0].ScalarString() == "root" && ida[0].IsUnquoted() {
|
||||
return hasBoard(root, ida[1:])
|
||||
}
|
||||
id := ida[0]
|
||||
if len(ida) == 1 {
|
||||
return root.Name == id
|
||||
return root.Name == id.ScalarString()
|
||||
}
|
||||
nextID := ida[1]
|
||||
switch id {
|
||||
switch id.ScalarString() {
|
||||
case "layers":
|
||||
for _, b := range root.Layers {
|
||||
if b.Name == nextID {
|
||||
if b.Name == nextID.ScalarString() {
|
||||
return hasBoard(b, ida[2:])
|
||||
}
|
||||
}
|
||||
case "scenarios":
|
||||
for _, b := range root.Scenarios {
|
||||
if b.Name == nextID {
|
||||
if b.Name == nextID.ScalarString() {
|
||||
return hasBoard(b, ida[2:])
|
||||
}
|
||||
}
|
||||
case "steps":
|
||||
for _, b := range root.Steps {
|
||||
if b.Name == nextID {
|
||||
if b.Name == nextID.ScalarString() {
|
||||
return hasBoard(b, ida[2:])
|
||||
}
|
||||
}
|
||||
|
|
@ -1257,20 +1290,10 @@ func init() {
|
|||
}
|
||||
}
|
||||
|
||||
func d2graphIDA(irIDA []string) (ida []string) {
|
||||
for _, el := range irIDA {
|
||||
n := &d2ast.KeyPath{
|
||||
Path: []*d2ast.StringBox{d2ast.MakeValueBox(d2ast.RawString(el, true)).StringBox()},
|
||||
}
|
||||
ida = append(ida, d2format.Format(n))
|
||||
}
|
||||
return ida
|
||||
}
|
||||
|
||||
// Unused for now until shape: edge_group
|
||||
func (c *compiler) preprocessSeqDiagrams(m *d2ir.Map) {
|
||||
for _, f := range m.Fields {
|
||||
if f.Name == "shape" && f.Primary_.Value.ScalarString() == d2target.ShapeSequenceDiagram {
|
||||
if f.Name.ScalarString() == "shape" && f.Name.IsUnquoted() && f.Primary_.Value.ScalarString() == d2target.ShapeSequenceDiagram {
|
||||
c.preprocessEdgeGroup(m, m)
|
||||
return
|
||||
}
|
||||
|
|
@ -1322,8 +1345,8 @@ func (c *compiler) preprocessEdgeGroup(seqDiagram, m *d2ir.Map) {
|
|||
f := srcParent.GetField(el)
|
||||
if !isEdgeGroup(f) {
|
||||
for j := 0; j < i+1; j++ {
|
||||
e.ID.SrcPath = append([]string{"_"}, e.ID.SrcPath...)
|
||||
e.ID.DstPath = append([]string{"_"}, e.ID.DstPath...)
|
||||
e.ID.SrcPath = append([]d2ast.String{d2ast.FlatUnquotedString("_")}, e.ID.SrcPath...)
|
||||
e.ID.DstPath = append([]d2ast.String{d2ast.FlatUnquotedString("_")}, e.ID.DstPath...)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
|
@ -1338,7 +1361,7 @@ func hoistActor(seqDiagram *d2ir.Map, f *d2ir.Field) {
|
|||
seqDiagram.Fields = append(seqDiagram.Fields, f.Copy(seqDiagram).(*d2ir.Field))
|
||||
} else {
|
||||
d2ir.OverlayField(f2, f)
|
||||
d2ir.ParentMap(f).DeleteField(f.Name)
|
||||
d2ir.ParentMap(f).DeleteField(f.Name.ScalarString())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1383,7 +1406,7 @@ func parentSeqDiagram(n d2ir.Node) *d2ir.Map {
|
|||
return nil
|
||||
}
|
||||
for _, f := range m.Fields {
|
||||
if f.Name == "shape" && f.Primary_.Value.ScalarString() == d2target.ShapeSequenceDiagram {
|
||||
if f.Name.ScalarString() == "shape" && f.Name.IsUnquoted() && f.Primary_.Value.ScalarString() == d2target.ShapeSequenceDiagram {
|
||||
return m
|
||||
}
|
||||
}
|
||||
|
|
@ -1392,7 +1415,7 @@ func parentSeqDiagram(n d2ir.Node) *d2ir.Map {
|
|||
}
|
||||
|
||||
func compileConfig(ir *d2ir.Map) (*d2target.Config, error) {
|
||||
f := ir.GetField("vars", "d2-config")
|
||||
f := ir.GetField(d2ast.FlatUnquotedString("vars"), d2ast.FlatUnquotedString("d2-config"))
|
||||
if f == nil || f.Map() == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
|
@ -1401,36 +1424,36 @@ func compileConfig(ir *d2ir.Map) (*d2target.Config, error) {
|
|||
|
||||
config := &d2target.Config{}
|
||||
|
||||
f = configMap.GetField("sketch")
|
||||
f = configMap.GetField(d2ast.FlatUnquotedString("sketch"))
|
||||
if f != nil {
|
||||
val, _ := strconv.ParseBool(f.Primary().Value.ScalarString())
|
||||
config.Sketch = &val
|
||||
}
|
||||
|
||||
f = configMap.GetField("theme-id")
|
||||
f = configMap.GetField(d2ast.FlatUnquotedString("theme-id"))
|
||||
if f != nil {
|
||||
val, _ := strconv.Atoi(f.Primary().Value.ScalarString())
|
||||
config.ThemeID = go2.Pointer(int64(val))
|
||||
}
|
||||
|
||||
f = configMap.GetField("dark-theme-id")
|
||||
f = configMap.GetField(d2ast.FlatUnquotedString("dark-theme-id"))
|
||||
if f != nil {
|
||||
val, _ := strconv.Atoi(f.Primary().Value.ScalarString())
|
||||
config.DarkThemeID = go2.Pointer(int64(val))
|
||||
}
|
||||
|
||||
f = configMap.GetField("pad")
|
||||
f = configMap.GetField(d2ast.FlatUnquotedString("pad"))
|
||||
if f != nil {
|
||||
val, _ := strconv.Atoi(f.Primary().Value.ScalarString())
|
||||
config.Pad = go2.Pointer(int64(val))
|
||||
}
|
||||
|
||||
f = configMap.GetField("layout-engine")
|
||||
f = configMap.GetField(d2ast.FlatUnquotedString("layout-engine"))
|
||||
if f != nil {
|
||||
config.LayoutEngine = go2.Pointer(f.Primary().Value.ScalarString())
|
||||
}
|
||||
|
||||
f = configMap.GetField("theme-overrides")
|
||||
f = configMap.GetField(d2ast.FlatUnquotedString("theme-overrides"))
|
||||
if f != nil {
|
||||
overrides, err := compileThemeOverrides(f.Map())
|
||||
if err != nil {
|
||||
|
|
@ -1438,7 +1461,7 @@ func compileConfig(ir *d2ir.Map) (*d2target.Config, error) {
|
|||
}
|
||||
config.ThemeOverrides = overrides
|
||||
}
|
||||
f = configMap.GetField("dark-theme-overrides")
|
||||
f = configMap.GetField(d2ast.FlatUnquotedString("dark-theme-overrides"))
|
||||
if f != nil {
|
||||
overrides, err := compileThemeOverrides(f.Map())
|
||||
if err != nil {
|
||||
|
|
@ -1446,6 +1469,27 @@ func compileConfig(ir *d2ir.Map) (*d2target.Config, error) {
|
|||
}
|
||||
config.DarkThemeOverrides = overrides
|
||||
}
|
||||
f = configMap.GetField(d2ast.FlatUnquotedString("data"))
|
||||
if f != nil && f.Map() != nil {
|
||||
config.Data = make(map[string]interface{})
|
||||
for _, f := range f.Map().Fields {
|
||||
if f.Primary() != nil {
|
||||
config.Data[f.Name.ScalarString()] = f.Primary().Value.ScalarString()
|
||||
} else if f.Composite != nil {
|
||||
var arr []interface{}
|
||||
switch c := f.Composite.(type) {
|
||||
case *d2ir.Array:
|
||||
for _, f := range c.Values {
|
||||
switch c := f.(type) {
|
||||
case *d2ir.Scalar:
|
||||
arr = append(arr, c.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
config.Data[f.Name.ScalarString()] = arr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
|
@ -1459,7 +1503,7 @@ func compileThemeOverrides(m *d2ir.Map) (*d2target.ThemeOverrides, error) {
|
|||
err := &d2parser.ParseError{}
|
||||
FOR:
|
||||
for _, f := range m.Fields {
|
||||
switch strings.ToUpper(f.Name) {
|
||||
switch strings.ToUpper(f.Name.ScalarString()) {
|
||||
case "N1":
|
||||
themeOverrides.N1 = go2.Pointer(f.Primary().Value.ScalarString())
|
||||
case "N2":
|
||||
|
|
@ -1497,11 +1541,11 @@ FOR:
|
|||
case "AB5":
|
||||
themeOverrides.AB5 = go2.Pointer(f.Primary().Value.ScalarString())
|
||||
default:
|
||||
err.Errors = append(err.Errors, d2parser.Errorf(f.LastPrimaryKey(), fmt.Sprintf(`"%s" is not a valid theme code`, f.Name)).(d2ast.Error))
|
||||
err.Errors = append(err.Errors, d2parser.Errorf(f.LastPrimaryKey(), fmt.Sprintf(`"%s" is not a valid theme code`, f.Name.ScalarString())).(d2ast.Error))
|
||||
continue FOR
|
||||
}
|
||||
if !go2.Contains(color.NamedColors, strings.ToLower(f.Primary().Value.ScalarString())) && !color.ColorHexRegex.MatchString(f.Primary().Value.ScalarString()) {
|
||||
err.Errors = append(err.Errors, d2parser.Errorf(f.LastPrimaryKey(), fmt.Sprintf(`expected "%s" to be a valid named color ("orange") or a hex code ("#f0ff3a")`, f.Name)).(d2ast.Error))
|
||||
err.Errors = append(err.Errors, d2parser.Errorf(f.LastPrimaryKey(), fmt.Sprintf(`expected "%s" to be a valid named color ("orange") or a hex code ("#f0ff3a")`, f.Name.ScalarString())).(d2ast.Error))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -194,6 +194,9 @@ func toShape(obj *d2graph.Object, g *d2graph.Graph) d2target.Shape {
|
|||
if obj.Tooltip != nil {
|
||||
shape.Tooltip = obj.Tooltip.Value
|
||||
}
|
||||
if obj.Style.Animated != nil {
|
||||
shape.Animated, _ = strconv.ParseBool(obj.Style.Animated.Value)
|
||||
}
|
||||
if obj.Link != nil {
|
||||
shape.Link = obj.Link.Value
|
||||
shape.PrettyLink = toPrettyLink(g, obj.Link.Value)
|
||||
|
|
@ -354,6 +357,9 @@ func toConnection(edge *d2graph.Edge, theme *d2themes.Theme) d2target.Connection
|
|||
if edge.Style.Font != nil {
|
||||
connection.FontFamily = edge.Style.Font.Value
|
||||
}
|
||||
if edge.Link != nil {
|
||||
connection.Link = edge.Link.Value
|
||||
}
|
||||
connection.Label = text.Text
|
||||
connection.LabelWidth = text.Dimensions.Width
|
||||
connection.LabelHeight = text.Dimensions.Height
|
||||
|
|
|
|||
|
|
@ -2,12 +2,11 @@ package d2exporter_test
|
|||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"cdr.dev/slog"
|
||||
|
||||
tassert "github.com/stretchr/testify/assert"
|
||||
|
||||
"oss.terrastruct.com/util-go/assert"
|
||||
|
|
@ -218,7 +217,7 @@ func runa(t *testing.T, tcs []testCase) {
|
|||
|
||||
func run(t *testing.T, tc testCase) {
|
||||
ctx := context.Background()
|
||||
ctx = log.WithTB(ctx, t, nil)
|
||||
ctx = log.WithTB(ctx, t)
|
||||
ctx = log.Leveled(ctx, slog.LevelDebug)
|
||||
|
||||
g, config, err := d2compiler.Compile("", strings.NewReader(tc.dsl), &d2compiler.CompileOptions{
|
||||
|
|
@ -277,7 +276,7 @@ func run(t *testing.T, tc testCase) {
|
|||
// TestHashID tests that 2 diagrams with different theme configs do not equal each other
|
||||
func TestHashID(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx = log.WithTB(ctx, t, nil)
|
||||
ctx = log.WithTB(ctx, t)
|
||||
ctx = log.Leveled(ctx, slog.LevelDebug)
|
||||
|
||||
aString := `
|
||||
|
|
@ -304,10 +303,10 @@ a -> b
|
|||
db, err := compile(ctx, bString)
|
||||
assert.JSON(t, nil, err)
|
||||
|
||||
hashA, err := da.HashID()
|
||||
hashA, err := da.HashID(nil)
|
||||
assert.JSON(t, nil, err)
|
||||
|
||||
hashB, err := db.HashID()
|
||||
hashB, err := db.HashID(nil)
|
||||
assert.JSON(t, nil, err)
|
||||
|
||||
assert.NotEqual(t, hashA, hashB)
|
||||
|
|
|
|||
|
|
@ -135,6 +135,12 @@ func (p *printer) interpolationBoxes(boxes []d2ast.InterpolationBox, isDoubleStr
|
|||
}
|
||||
b.StringRaw = &s
|
||||
}
|
||||
if !isDoubleString {
|
||||
if _, ok := d2ast.ReservedKeywords[strings.ToLower(*b.StringRaw)]; ok {
|
||||
s := strings.ToLower(*b.StringRaw)
|
||||
b.StringRaw = &s
|
||||
}
|
||||
}
|
||||
p.sb.WriteString(*b.StringRaw)
|
||||
}
|
||||
}
|
||||
|
|
@ -287,11 +293,18 @@ func (p *printer) _map(m *d2ast.Map) {
|
|||
if nb.IsBoardNode() {
|
||||
switch nb.MapKey.Key.Path[0].Unbox().ScalarString() {
|
||||
case "layers":
|
||||
layerNodes = append(layerNodes, nb)
|
||||
// remove useless
|
||||
if nb.MapKey.Value.Map != nil && len(nb.MapKey.Value.Map.Nodes) > 0 {
|
||||
layerNodes = append(layerNodes, nb)
|
||||
}
|
||||
case "scenarios":
|
||||
scenarioNodes = append(scenarioNodes, nb)
|
||||
if nb.MapKey.Value.Map != nil && len(nb.MapKey.Value.Map.Nodes) > 0 {
|
||||
scenarioNodes = append(scenarioNodes, nb)
|
||||
}
|
||||
case "steps":
|
||||
stepNodes = append(stepNodes, nb)
|
||||
if nb.MapKey.Value.Map != nil && len(nb.MapKey.Value.Map.Nodes) > 0 {
|
||||
stepNodes = append(stepNodes, nb)
|
||||
}
|
||||
}
|
||||
prev = n
|
||||
continue
|
||||
|
|
@ -333,12 +346,14 @@ func (p *printer) _map(m *d2ast.Map) {
|
|||
n := boards[i].Unbox()
|
||||
// if this board is the very first line of the file, don't add an extra newline
|
||||
if n.GetRange().Start.Line != 0 {
|
||||
p.newline()
|
||||
p.sb.WriteByte('\n')
|
||||
}
|
||||
// if scope only has boards, don't newline the first board
|
||||
if i != 0 || len(m.Nodes) > len(boards) {
|
||||
p.newline()
|
||||
p.sb.WriteByte('\n')
|
||||
}
|
||||
|
||||
p.sb.WriteString(p.indentStr)
|
||||
p.node(n)
|
||||
prev = n
|
||||
}
|
||||
|
|
@ -358,6 +373,9 @@ func (p *printer) _map(m *d2ast.Map) {
|
|||
func (p *printer) mapKey(mk *d2ast.Key) {
|
||||
if mk.Ampersand {
|
||||
p.sb.WriteByte('&')
|
||||
} else if mk.NotAmpersand {
|
||||
p.sb.WriteByte('!')
|
||||
p.sb.WriteByte('&')
|
||||
}
|
||||
if mk.Key != nil {
|
||||
p.key(mk.Key)
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@ x -> y
|
|||
exp: `x -> y
|
||||
`,
|
||||
},
|
||||
|
||||
{
|
||||
name: "complex",
|
||||
in: `
|
||||
|
|
@ -153,7 +152,7 @@ meow
|
|||
diagram: int {constraint: foreign_key}
|
||||
}
|
||||
meow <- diagrams.id
|
||||
|
||||
|
||||
steps: {
|
||||
shape: sql_table
|
||||
id: {type: int; constraint: primary_key}
|
||||
|
|
@ -665,8 +664,8 @@ x: @"x/../file"
|
|||
b: {
|
||||
e
|
||||
scenarios: {
|
||||
p: {
|
||||
x
|
||||
p: {
|
||||
x
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -678,14 +677,14 @@ x: @"x/../file"
|
|||
exp: `layers: {
|
||||
b: {
|
||||
e
|
||||
|
||||
|
||||
scenarios: {
|
||||
p: {
|
||||
x
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
steps: {
|
||||
a
|
||||
}
|
||||
|
|
@ -763,7 +762,7 @@ only-layers: {
|
|||
X
|
||||
Y
|
||||
}
|
||||
|
||||
|
||||
layers: {
|
||||
Z
|
||||
}
|
||||
|
|
@ -773,10 +772,10 @@ layers: {
|
|||
Test super nested: {
|
||||
base-layer
|
||||
last-layer
|
||||
|
||||
|
||||
layers: {
|
||||
layer-board
|
||||
|
||||
|
||||
layers: {
|
||||
grand-child-layer: {
|
||||
grand-child-board
|
||||
|
|
@ -789,7 +788,7 @@ layers: {
|
|||
scenarios: {
|
||||
scenario-1: {
|
||||
non-step
|
||||
|
||||
|
||||
steps: {
|
||||
step-1: {
|
||||
Test
|
||||
|
|
@ -829,6 +828,70 @@ mybox: {
|
|||
mybox: {
|
||||
label: prefix${test}suffix
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "not-filter",
|
||||
in: `jacob: {
|
||||
shape: circle
|
||||
}
|
||||
jeremy: {
|
||||
shape: rectangle
|
||||
}
|
||||
*: {
|
||||
!&shape: rectangle
|
||||
label: I'm not a rectangle
|
||||
}`,
|
||||
exp: `jacob: {
|
||||
shape: circle
|
||||
}
|
||||
jeremy: {
|
||||
shape: rectangle
|
||||
}
|
||||
*: {
|
||||
!&shape: rectangle
|
||||
label: I'm not a rectangle
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "lowercase-reserved",
|
||||
in: `jacob: {
|
||||
SHAPE: circle
|
||||
}
|
||||
jeremy.SHAPE: rectangle
|
||||
alice.STYLE.fill: red
|
||||
bob.style.FILL: red
|
||||
carmen.STYLE.FILL: red
|
||||
coop: {
|
||||
STYLE: {
|
||||
FILL: blue
|
||||
}
|
||||
}
|
||||
`,
|
||||
exp: `jacob: {
|
||||
shape: circle
|
||||
}
|
||||
jeremy.shape: rectangle
|
||||
alice.style.fill: red
|
||||
bob.style.fill: red
|
||||
carmen.style.fill: red
|
||||
coop: {
|
||||
style: {
|
||||
fill: blue
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "remove-empty-boards",
|
||||
in: `k
|
||||
|
||||
layers
|
||||
scenarios: {}
|
||||
steps: asdf
|
||||
`,
|
||||
exp: `k
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,6 +60,11 @@ type Graph struct {
|
|||
|
||||
// Object.Level uses the location of a nested graph
|
||||
RootLevel int `json:"rootLevel,omitempty"`
|
||||
|
||||
// Currently this holds data embedded from source code configuration variables
|
||||
// Plugins only have access to exported graph, so this data structure allows
|
||||
// carrying arbitrary metadata that any plugin might handle
|
||||
Data map[string]interface{} `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
func NewGraph() *Graph {
|
||||
|
|
@ -79,6 +84,66 @@ func (g *Graph) RootBoard() *Graph {
|
|||
return g
|
||||
}
|
||||
|
||||
func (g *Graph) IDA() []string {
|
||||
if g == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var parts []string
|
||||
|
||||
current := g
|
||||
for current != nil {
|
||||
if current.Name != "" {
|
||||
parts = append(parts, current.Name)
|
||||
}
|
||||
current = current.Parent
|
||||
}
|
||||
|
||||
for i := 0; i < len(parts)/2; i++ {
|
||||
j := len(parts) - 1 - i
|
||||
parts[i], parts[j] = parts[j], parts[i]
|
||||
}
|
||||
|
||||
if len(parts) == 0 {
|
||||
return []string{"root"}
|
||||
}
|
||||
|
||||
parts = append([]string{"root"}, parts...)
|
||||
|
||||
if g.Parent != nil {
|
||||
var containerName string
|
||||
if len(g.Parent.Layers) > 0 {
|
||||
for _, l := range g.Parent.Layers {
|
||||
if l == g {
|
||||
containerName = "layers"
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(g.Parent.Scenarios) > 0 {
|
||||
for _, s := range g.Parent.Scenarios {
|
||||
if s == g {
|
||||
containerName = "scenarios"
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(g.Parent.Steps) > 0 {
|
||||
for _, s := range g.Parent.Steps {
|
||||
if s == g {
|
||||
containerName = "steps"
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if containerName != "" {
|
||||
parts = append(parts[:1], append([]string{containerName}, parts[1:]...)...)
|
||||
}
|
||||
}
|
||||
|
||||
return parts
|
||||
}
|
||||
|
||||
type LayoutGraph func(context.Context, *Graph) error
|
||||
type RouteEdges func(context.Context, *Graph, []*Edge) error
|
||||
|
||||
|
|
@ -197,6 +262,7 @@ type Reference struct {
|
|||
Scope *d2ast.Map `json:"-"`
|
||||
ScopeObj *Object `json:"-"`
|
||||
ScopeAST *d2ast.Map `json:"-"`
|
||||
IsVar bool `json:"-"`
|
||||
}
|
||||
|
||||
func (r Reference) MapKeyEdgeDest() bool {
|
||||
|
|
@ -252,24 +318,24 @@ func (s *Style) Apply(key, value string) error {
|
|||
if s.Stroke == nil {
|
||||
break
|
||||
}
|
||||
if !go2.Contains(color.NamedColors, strings.ToLower(value)) && !color.ColorHexRegex.MatchString(value) {
|
||||
return errors.New(`expected "stroke" to be a valid named color ("orange") or a hex code ("#f0ff3a")`)
|
||||
if !color.ValidColor(value) {
|
||||
return errors.New(`expected "stroke" to be a valid named color ("orange"), a hex code ("#f0ff3a"), or a gradient ("linear-gradient(red, blue)")`)
|
||||
}
|
||||
s.Stroke.Value = value
|
||||
case "fill":
|
||||
if s.Fill == nil {
|
||||
break
|
||||
}
|
||||
if !go2.Contains(color.NamedColors, strings.ToLower(value)) && !color.ColorHexRegex.MatchString(value) {
|
||||
return errors.New(`expected "fill" to be a valid named color ("orange") or a hex code ("#f0ff3a")`)
|
||||
if !color.ValidColor(value) {
|
||||
return errors.New(`expected "fill" to be a valid named color ("orange"), a hex code ("#f0ff3a"), or a gradient ("linear-gradient(red, blue)")`)
|
||||
}
|
||||
s.Fill.Value = value
|
||||
case "fill-pattern":
|
||||
if s.FillPattern == nil {
|
||||
break
|
||||
}
|
||||
if !go2.Contains(FillPatterns, strings.ToLower(value)) {
|
||||
return fmt.Errorf(`expected "fill-pattern" to be one of: %s`, strings.Join(FillPatterns, ", "))
|
||||
if !go2.Contains(d2ast.FillPatterns, strings.ToLower(value)) {
|
||||
return fmt.Errorf(`expected "fill-pattern" to be one of: %s`, strings.Join(d2ast.FillPatterns, ", "))
|
||||
}
|
||||
s.FillPattern.Value = value
|
||||
case "stroke-width":
|
||||
|
|
@ -347,8 +413,8 @@ func (s *Style) Apply(key, value string) error {
|
|||
if s.FontColor == nil {
|
||||
break
|
||||
}
|
||||
if !go2.Contains(color.NamedColors, strings.ToLower(value)) && !color.ColorHexRegex.MatchString(value) {
|
||||
return errors.New(`expected "font-color" to be a valid named color ("orange") or a hex code ("#f0ff3a")`)
|
||||
if !color.ValidColor(value) {
|
||||
return errors.New(`expected "font-color" to be a valid named color ("orange"), a hex code ("#f0ff3a"), or a gradient ("linear-gradient(red, blue)")`)
|
||||
}
|
||||
s.FontColor.Value = value
|
||||
case "animated":
|
||||
|
|
@ -409,8 +475,8 @@ func (s *Style) Apply(key, value string) error {
|
|||
if s.TextTransform == nil {
|
||||
break
|
||||
}
|
||||
if !go2.Contains(textTransforms, strings.ToLower(value)) {
|
||||
return fmt.Errorf(`expected "text-transform" to be one of (%s)`, strings.Join(textTransforms, ", "))
|
||||
if !go2.Contains(d2ast.TextTransforms, strings.ToLower(value)) {
|
||||
return fmt.Errorf(`expected "text-transform" to be one of (%s)`, strings.Join(d2ast.TextTransforms, ", "))
|
||||
}
|
||||
s.TextTransform.Value = value
|
||||
default:
|
||||
|
|
@ -628,7 +694,10 @@ func (obj *Object) Text() *d2target.MText {
|
|||
}
|
||||
}
|
||||
|
||||
func (obj *Object) newObject(id string) *Object {
|
||||
func (obj *Object) newObject(ids d2ast.String) *Object {
|
||||
id := d2format.Format(&d2ast.KeyPath{
|
||||
Path: []*d2ast.StringBox{d2ast.MakeValueBox(d2ast.RawString(ids.ScalarString(), true)).StringBox()},
|
||||
})
|
||||
idval := id
|
||||
k, _ := d2parser.ParseKey(id)
|
||||
if k != nil && len(k.Path) > 0 {
|
||||
|
|
@ -667,7 +736,7 @@ func (obj *Object) HasChild(ids []string) (*Object, bool) {
|
|||
return obj, true
|
||||
}
|
||||
if len(ids) == 1 && ids[0] != "style" {
|
||||
_, ok := ReservedKeywords[ids[0]]
|
||||
_, ok := d2ast.ReservedKeywords[ids[0]]
|
||||
if ok {
|
||||
return obj, true
|
||||
}
|
||||
|
|
@ -781,7 +850,7 @@ func (obj *Object) FindEdges(mk *d2ast.Key) ([]*Edge, bool) {
|
|||
return ea, true
|
||||
}
|
||||
|
||||
func (obj *Object) ensureChildEdge(ida []string) *Object {
|
||||
func (obj *Object) ensureChildEdge(ida []d2ast.String) *Object {
|
||||
for i := range ida {
|
||||
switch obj.Shape.Value {
|
||||
case d2target.ShapeClass, d2target.ShapeSQLTable:
|
||||
|
|
@ -797,12 +866,12 @@ func (obj *Object) ensureChildEdge(ida []string) *Object {
|
|||
|
||||
// EnsureChild grabs the child by ids or creates it if it does not exist including all
|
||||
// intermediate nodes.
|
||||
func (obj *Object) EnsureChild(ida []string) *Object {
|
||||
func (obj *Object) EnsureChild(ida []d2ast.String) *Object {
|
||||
seq := obj.OuterSequenceDiagram()
|
||||
if seq != nil {
|
||||
for _, c := range seq.ChildrenArray {
|
||||
if c.ID == ida[0] {
|
||||
if obj.ID == ida[0] {
|
||||
if c.ID == ida[0].ScalarString() {
|
||||
if obj.ID == ida[0].ScalarString() {
|
||||
// In cases of a.a where EnsureChild is called on the parent a, the second a should
|
||||
// be created as a child of a and not as a child of the diagram. This is super
|
||||
// unfortunate code but alas.
|
||||
|
|
@ -818,9 +887,11 @@ func (obj *Object) EnsureChild(ida []string) *Object {
|
|||
return obj
|
||||
}
|
||||
|
||||
_, is := ReservedKeywordHolders[ida[0]]
|
||||
_, is := d2ast.ReservedKeywordHolders[ida[0].ScalarString()]
|
||||
is = is && ida[0].IsUnquoted()
|
||||
if len(ida) == 1 && !is {
|
||||
_, ok := ReservedKeywords[ida[0]]
|
||||
_, ok := d2ast.ReservedKeywords[ida[0].ScalarString()]
|
||||
ok = ok && ida[0].IsUnquoted()
|
||||
if ok {
|
||||
return obj
|
||||
}
|
||||
|
|
@ -829,11 +900,14 @@ func (obj *Object) EnsureChild(ida []string) *Object {
|
|||
id := ida[0]
|
||||
ida = ida[1:]
|
||||
|
||||
if id == "_" {
|
||||
if id.ScalarString() == "_" && id.IsUnquoted() {
|
||||
return obj.Parent.EnsureChild(ida)
|
||||
}
|
||||
|
||||
child, ok := obj.Children[strings.ToLower(id)]
|
||||
head := d2format.Format(&d2ast.KeyPath{
|
||||
Path: []*d2ast.StringBox{d2ast.MakeValueBox(d2ast.RawString(id.ScalarString(), true)).StringBox()},
|
||||
})
|
||||
child, ok := obj.Children[strings.ToLower(head)]
|
||||
if !ok {
|
||||
child = obj.newObject(id)
|
||||
}
|
||||
|
|
@ -986,7 +1060,7 @@ func (obj *Object) GetDefaultSize(mtexts []*d2target.MText, ruler *textmeasure.R
|
|||
}
|
||||
if anyRowText != nil {
|
||||
rowHeight := GetTextDimensions(mtexts, ruler, anyRowText, go2.Pointer(d2fonts.SourceCodePro)).Height + d2target.VerticalPadding
|
||||
dims.Height = rowHeight * (len(obj.Class.Fields) + len(obj.Class.Methods) + 2)
|
||||
dims.Height = rowHeight*(len(obj.Class.Fields)+len(obj.Class.Methods)) + go2.Max(2*rowHeight, labelDims.Height+2*label.PADDING)
|
||||
} else {
|
||||
dims.Height = 2*go2.Max(12, labelDims.Height) + d2target.VerticalPadding
|
||||
}
|
||||
|
|
@ -1120,7 +1194,7 @@ func (obj *Object) IsConstantNear() bool {
|
|||
if isKey {
|
||||
return false
|
||||
}
|
||||
_, isConst := NearConstants[keyPath[0]]
|
||||
_, isConst := d2ast.NearConstants[keyPath[0]]
|
||||
return isConst
|
||||
}
|
||||
|
||||
|
|
@ -1232,10 +1306,10 @@ func (e *Edge) AbsID() string {
|
|||
return fmt.Sprintf("%s(%s %s %s)[%d]", commonKey, strings.Join(srcIDA, "."), e.ArrowString(), strings.Join(dstIDA, "."), e.Index)
|
||||
}
|
||||
|
||||
func (obj *Object) Connect(srcID, dstID []string, srcArrow, dstArrow bool, label string) (*Edge, error) {
|
||||
for _, id := range [][]string{srcID, dstID} {
|
||||
func (obj *Object) Connect(srcID, dstID []d2ast.String, srcArrow, dstArrow bool, label string) (*Edge, error) {
|
||||
for _, id := range [][]d2ast.String{srcID, dstID} {
|
||||
for _, p := range id {
|
||||
if _, ok := ReservedKeywords[p]; ok {
|
||||
if _, ok := d2ast.ReservedKeywords[p.ScalarString()]; ok && p.IsUnquoted() {
|
||||
return nil, errors.New("cannot connect to reserved keyword")
|
||||
}
|
||||
}
|
||||
|
|
@ -1263,7 +1337,7 @@ func (obj *Object) Connect(srcID, dstID []string, srcArrow, dstArrow bool, label
|
|||
return e, nil
|
||||
}
|
||||
|
||||
func addSQLTableColumnIndices(e *Edge, srcID, dstID []string, obj, src, dst *Object) {
|
||||
func addSQLTableColumnIndices(e *Edge, srcID, dstID []d2ast.String, obj, src, dst *Object) {
|
||||
if src.Shape.Value == d2target.ShapeSQLTable {
|
||||
if src == dst {
|
||||
// Ignore edge to column inside table.
|
||||
|
|
@ -1273,7 +1347,7 @@ func addSQLTableColumnIndices(e *Edge, srcID, dstID []string, obj, src, dst *Obj
|
|||
srcAbsID := src.AbsIDArray()
|
||||
if len(objAbsID)+len(srcID) > len(srcAbsID) {
|
||||
for i, d2col := range src.SQLTable.Columns {
|
||||
if d2col.Name.Label == srcID[len(srcID)-1] {
|
||||
if d2col.Name.Label == srcID[len(srcID)-1].ScalarString() {
|
||||
d2col.Reference = dst.AbsID()
|
||||
e.SrcTableColumnIndex = new(int)
|
||||
*e.SrcTableColumnIndex = i
|
||||
|
|
@ -1287,7 +1361,7 @@ func addSQLTableColumnIndices(e *Edge, srcID, dstID []string, obj, src, dst *Obj
|
|||
dstAbsID := dst.AbsIDArray()
|
||||
if len(objAbsID)+len(dstID) > len(dstAbsID) {
|
||||
for i, d2col := range dst.SQLTable.Columns {
|
||||
if d2col.Name.Label == dstID[len(dstID)-1] {
|
||||
if d2col.Name.Label == dstID[len(dstID)-1].ScalarString() {
|
||||
d2col.Reference = dst.AbsID()
|
||||
e.DstTableColumnIndex = new(int)
|
||||
*e.DstTableColumnIndex = i
|
||||
|
|
@ -1428,12 +1502,12 @@ func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler
|
|||
// user-specified label/icon positions
|
||||
if obj.HasLabel() && obj.Attributes.LabelPosition != nil {
|
||||
scalar := *obj.Attributes.LabelPosition
|
||||
position := LabelPositionsMapping[scalar.Value]
|
||||
position := d2ast.LabelPositionsMapping[scalar.Value]
|
||||
obj.LabelPosition = go2.Pointer(position.String())
|
||||
}
|
||||
if obj.Icon != nil && obj.Attributes.IconPosition != nil {
|
||||
scalar := *obj.Attributes.IconPosition
|
||||
position := LabelPositionsMapping[scalar.Value]
|
||||
position := d2ast.LabelPositionsMapping[scalar.Value]
|
||||
obj.IconPosition = go2.Pointer(position.String())
|
||||
}
|
||||
|
||||
|
|
@ -1538,11 +1612,8 @@ func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler
|
|||
switch shapeType {
|
||||
case shape.TABLE_TYPE, shape.CLASS_TYPE, shape.CODE_TYPE:
|
||||
default:
|
||||
if obj.Link != nil {
|
||||
paddingX += 32
|
||||
}
|
||||
if obj.Tooltip != nil {
|
||||
paddingX += 32
|
||||
if obj.Link != nil && obj.Tooltip != nil {
|
||||
paddingX += 64
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1673,205 +1744,6 @@ func Key(k *d2ast.KeyPath) []string {
|
|||
return d2format.KeyPath(k)
|
||||
}
|
||||
|
||||
// All reserved keywords. See init below.
|
||||
var ReservedKeywords map[string]struct{}
|
||||
|
||||
// Non Style/Holder keywords.
|
||||
var SimpleReservedKeywords = map[string]struct{}{
|
||||
"label": {},
|
||||
"desc": {},
|
||||
"shape": {},
|
||||
"icon": {},
|
||||
"constraint": {},
|
||||
"tooltip": {},
|
||||
"link": {},
|
||||
"near": {},
|
||||
"width": {},
|
||||
"height": {},
|
||||
"direction": {},
|
||||
"top": {},
|
||||
"left": {},
|
||||
"grid-rows": {},
|
||||
"grid-columns": {},
|
||||
"grid-gap": {},
|
||||
"vertical-gap": {},
|
||||
"horizontal-gap": {},
|
||||
"class": {},
|
||||
"vars": {},
|
||||
}
|
||||
|
||||
// ReservedKeywordHolders are reserved keywords that are meaningless on its own and must hold composites
|
||||
var ReservedKeywordHolders = map[string]struct{}{
|
||||
"style": {},
|
||||
"source-arrowhead": {},
|
||||
"target-arrowhead": {},
|
||||
}
|
||||
|
||||
// CompositeReservedKeywords are reserved keywords that can hold composites
|
||||
var CompositeReservedKeywords = map[string]struct{}{
|
||||
"classes": {},
|
||||
"constraint": {},
|
||||
"label": {},
|
||||
"icon": {},
|
||||
}
|
||||
|
||||
// StyleKeywords are reserved keywords which cannot exist outside of the "style" keyword
|
||||
var StyleKeywords = map[string]struct{}{
|
||||
"opacity": {},
|
||||
"stroke": {},
|
||||
"fill": {},
|
||||
"fill-pattern": {},
|
||||
"stroke-width": {},
|
||||
"stroke-dash": {},
|
||||
"border-radius": {},
|
||||
|
||||
// Only for text
|
||||
"font": {},
|
||||
"font-size": {},
|
||||
"font-color": {},
|
||||
"bold": {},
|
||||
"italic": {},
|
||||
"underline": {},
|
||||
"text-transform": {},
|
||||
|
||||
// Only for shapes
|
||||
"shadow": {},
|
||||
"multiple": {},
|
||||
"double-border": {},
|
||||
|
||||
// Only for squares
|
||||
"3d": {},
|
||||
|
||||
// Only for edges
|
||||
"animated": {},
|
||||
"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{}
|
||||
|
||||
// LabelPositionsArray are the values that labels and icons can set `near` to
|
||||
var LabelPositionsArray = []string{
|
||||
"top-left",
|
||||
"top-center",
|
||||
"top-right",
|
||||
|
||||
"center-left",
|
||||
"center-center",
|
||||
"center-right",
|
||||
|
||||
"bottom-left",
|
||||
"bottom-center",
|
||||
"bottom-right",
|
||||
|
||||
"outside-top-left",
|
||||
"outside-top-center",
|
||||
"outside-top-right",
|
||||
|
||||
"outside-left-top",
|
||||
"outside-left-center",
|
||||
"outside-left-bottom",
|
||||
|
||||
"outside-right-top",
|
||||
"outside-right-center",
|
||||
"outside-right-bottom",
|
||||
|
||||
"outside-bottom-left",
|
||||
"outside-bottom-center",
|
||||
"outside-bottom-right",
|
||||
}
|
||||
var LabelPositions map[string]struct{}
|
||||
|
||||
// convert to label.Position
|
||||
var LabelPositionsMapping = map[string]label.Position{
|
||||
"top-left": label.InsideTopLeft,
|
||||
"top-center": label.InsideTopCenter,
|
||||
"top-right": label.InsideTopRight,
|
||||
|
||||
"center-left": label.InsideMiddleLeft,
|
||||
"center-center": label.InsideMiddleCenter,
|
||||
"center-right": label.InsideMiddleRight,
|
||||
|
||||
"bottom-left": label.InsideBottomLeft,
|
||||
"bottom-center": label.InsideBottomCenter,
|
||||
"bottom-right": label.InsideBottomRight,
|
||||
|
||||
"outside-top-left": label.OutsideTopLeft,
|
||||
"outside-top-center": label.OutsideTopCenter,
|
||||
"outside-top-right": label.OutsideTopRight,
|
||||
|
||||
"outside-left-top": label.OutsideLeftTop,
|
||||
"outside-left-center": label.OutsideLeftMiddle,
|
||||
"outside-left-bottom": label.OutsideLeftBottom,
|
||||
|
||||
"outside-right-top": label.OutsideRightTop,
|
||||
"outside-right-center": label.OutsideRightMiddle,
|
||||
"outside-right-bottom": label.OutsideRightBottom,
|
||||
|
||||
"outside-bottom-left": label.OutsideBottomLeft,
|
||||
"outside-bottom-center": label.OutsideBottomCenter,
|
||||
"outside-bottom-right": label.OutsideBottomRight,
|
||||
}
|
||||
|
||||
var FillPatterns = []string{
|
||||
"none",
|
||||
"dots",
|
||||
"lines",
|
||||
"grain",
|
||||
"paper",
|
||||
}
|
||||
|
||||
var textTransforms = []string{"none", "uppercase", "lowercase", "capitalize"}
|
||||
|
||||
// BoardKeywords contains the keywords that create new boards.
|
||||
var BoardKeywords = map[string]struct{}{
|
||||
"layers": {},
|
||||
"scenarios": {},
|
||||
"steps": {},
|
||||
}
|
||||
|
||||
func init() {
|
||||
ReservedKeywords = make(map[string]struct{})
|
||||
for k, v := range SimpleReservedKeywords {
|
||||
ReservedKeywords[k] = v
|
||||
}
|
||||
for k, v := range StyleKeywords {
|
||||
ReservedKeywords[k] = v
|
||||
}
|
||||
for k, v := range ReservedKeywordHolders {
|
||||
CompositeReservedKeywords[k] = v
|
||||
}
|
||||
for k, v := range BoardKeywords {
|
||||
CompositeReservedKeywords[k] = v
|
||||
}
|
||||
for k, v := range CompositeReservedKeywords {
|
||||
ReservedKeywords[k] = v
|
||||
}
|
||||
|
||||
NearConstants = make(map[string]struct{}, len(NearConstantsArray))
|
||||
for _, k := range NearConstantsArray {
|
||||
NearConstants[k] = struct{}{}
|
||||
}
|
||||
|
||||
LabelPositions = make(map[string]struct{}, len(LabelPositionsArray))
|
||||
for _, k := range LabelPositionsArray {
|
||||
LabelPositions[k] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Graph) GetBoard(name string) *Graph {
|
||||
for _, l := range g.Layers {
|
||||
if l.Name == name {
|
||||
|
|
@ -1901,6 +1773,11 @@ func (g *Graph) SortObjectsByAST() {
|
|||
}
|
||||
r1 := o1.References[0]
|
||||
r2 := o2.References[0]
|
||||
// If they are variable substitutions, leave them alone, as their
|
||||
// references reflect where the variable is, not where the substitution is
|
||||
if r1.IsVar || r2.IsVar {
|
||||
return i < j
|
||||
}
|
||||
return r1.Key.Path[r1.KeyPathIndex].Unbox().GetRange().Before(r2.Key.Path[r2.KeyPathIndex].Unbox().GetRange())
|
||||
})
|
||||
g.Objects = objects
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package d2graph
|
||||
|
||||
import "oss.terrastruct.com/d2/d2target"
|
||||
import (
|
||||
"oss.terrastruct.com/d2/d2target"
|
||||
)
|
||||
|
||||
func (obj *Object) IsSequenceDiagram() bool {
|
||||
return obj != nil && obj.Shape.Value == d2target.ShapeSequenceDiagram
|
||||
|
|
|
|||
|
|
@ -10,10 +10,11 @@ import (
|
|||
)
|
||||
|
||||
type SerializedGraph struct {
|
||||
Root SerializedObject `json:"root"`
|
||||
Edges []SerializedEdge `json:"edges"`
|
||||
Objects []SerializedObject `json:"objects"`
|
||||
RootLevel int `json:"rootLevel"`
|
||||
Root SerializedObject `json:"root"`
|
||||
Edges []SerializedEdge `json:"edges"`
|
||||
Objects []SerializedObject `json:"objects"`
|
||||
RootLevel int `json:"rootLevel"`
|
||||
Data map[string]interface{} `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
type SerializedObject map[string]interface{}
|
||||
|
|
@ -27,6 +28,7 @@ func DeserializeGraph(bytes []byte, g *Graph) error {
|
|||
return err
|
||||
}
|
||||
|
||||
g.Data = sg.Data
|
||||
var root Object
|
||||
Convert(sg.Root, &root)
|
||||
g.Root = &root
|
||||
|
|
@ -95,6 +97,7 @@ func SerializeGraph(g *Graph) ([]byte, error) {
|
|||
}
|
||||
sg.Root = root
|
||||
sg.RootLevel = g.RootLevel
|
||||
sg.Data = g.Data
|
||||
|
||||
var sobjects []SerializedObject
|
||||
for _, o := range g.Objects {
|
||||
|
|
|
|||
423
d2ir/compile.go
423
d2ir/compile.go
|
|
@ -1,7 +1,10 @@
|
|||
package d2ir
|
||||
|
||||
import (
|
||||
"html"
|
||||
"io/fs"
|
||||
"net/url"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
|
|
@ -12,6 +15,7 @@ import (
|
|||
"oss.terrastruct.com/d2/d2parser"
|
||||
"oss.terrastruct.com/d2/d2themes"
|
||||
"oss.terrastruct.com/d2/d2themes/d2themescatalog"
|
||||
"oss.terrastruct.com/d2/lib/textmeasure"
|
||||
)
|
||||
|
||||
type globContext struct {
|
||||
|
|
@ -84,12 +88,12 @@ func Compile(ast *d2ast.Map, opts *CompileOptions) (*Map, []string, error) {
|
|||
}
|
||||
|
||||
func (c *compiler) overlayClasses(m *Map) {
|
||||
classes := m.GetField("classes")
|
||||
classes := m.GetField(d2ast.FlatUnquotedString("classes"))
|
||||
if classes == nil || classes.Map() == nil {
|
||||
return
|
||||
}
|
||||
|
||||
layersField := m.GetField("layers")
|
||||
layersField := m.GetField(d2ast.FlatUnquotedString("layers"))
|
||||
if layersField == nil {
|
||||
return
|
||||
}
|
||||
|
|
@ -100,11 +104,10 @@ func (c *compiler) overlayClasses(m *Map) {
|
|||
|
||||
for _, lf := range layers.Fields {
|
||||
if lf.Map() == nil || lf.Primary() != nil {
|
||||
c.errorf(lf.References[0].Context_.Key, "invalid layer")
|
||||
continue
|
||||
}
|
||||
l := lf.Map()
|
||||
lClasses := l.GetField("classes")
|
||||
lClasses := l.GetField(d2ast.FlatUnquotedString("classes"))
|
||||
|
||||
if lClasses == nil {
|
||||
lClasses = classes.Copy(l).(*Field)
|
||||
|
|
@ -122,7 +125,10 @@ func (c *compiler) overlayClasses(m *Map) {
|
|||
|
||||
func (c *compiler) compileSubstitutions(m *Map, varsStack []*Map) {
|
||||
for _, f := range m.Fields {
|
||||
if f.Name == "vars" && f.Map() != nil {
|
||||
if f.Name == nil {
|
||||
continue
|
||||
}
|
||||
if f.Name.ScalarString() == "vars" && f.Name.IsUnquoted() && f.Map() != nil {
|
||||
varsStack = append([]*Map{f.Map()}, varsStack...)
|
||||
}
|
||||
}
|
||||
|
|
@ -144,10 +150,9 @@ func (c *compiler) compileSubstitutions(m *Map, varsStack []*Map) {
|
|||
}
|
||||
}
|
||||
} else if f.Map() != nil {
|
||||
// don't resolve substitutions in vars with the current scope of vars
|
||||
if f.Name == "vars" {
|
||||
c.compileSubstitutions(f.Map(), varsStack[1:])
|
||||
c.validateConfigs(f.Map().GetField("d2-config"))
|
||||
if f.Name != nil && f.Name.ScalarString() == "vars" && f.Name.IsUnquoted() {
|
||||
c.compileSubstitutions(f.Map(), varsStack)
|
||||
c.validateConfigs(f.Map().GetField(d2ast.FlatUnquotedString("d2-config")))
|
||||
} else {
|
||||
c.compileSubstitutions(f.Map(), varsStack)
|
||||
}
|
||||
|
|
@ -169,37 +174,37 @@ func (c *compiler) validateConfigs(configs *Field) {
|
|||
}
|
||||
|
||||
if NodeBoardKind(ParentMap(ParentMap(configs))) == "" {
|
||||
c.errorf(configs.LastRef().AST(), `"%s" can only appear at root vars`, configs.Name)
|
||||
c.errorf(configs.LastRef().AST(), `"%s" can only appear at root vars`, configs.Name.ScalarString())
|
||||
return
|
||||
}
|
||||
|
||||
for _, f := range configs.Map().Fields {
|
||||
var val string
|
||||
if f.Primary() == nil {
|
||||
if f.Name != "theme-overrides" && f.Name != "dark-theme-overrides" {
|
||||
c.errorf(f.LastRef().AST(), `"%s" needs a value`, f.Name)
|
||||
if f.Name.ScalarString() != "theme-overrides" && f.Name.ScalarString() != "dark-theme-overrides" && f.Name.ScalarString() != "data" {
|
||||
c.errorf(f.LastRef().AST(), `"%s" needs a value`, f.Name.ScalarString())
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
val = f.Primary().Value.ScalarString()
|
||||
}
|
||||
|
||||
switch f.Name {
|
||||
switch f.Name.ScalarString() {
|
||||
case "sketch", "center":
|
||||
_, err := strconv.ParseBool(val)
|
||||
if err != nil {
|
||||
c.errorf(f.LastRef().AST(), `expected a boolean for "%s", got "%s"`, f.Name, val)
|
||||
c.errorf(f.LastRef().AST(), `expected a boolean for "%s", got "%s"`, f.Name.ScalarString(), val)
|
||||
continue
|
||||
}
|
||||
case "theme-overrides", "dark-theme-overrides":
|
||||
case "theme-overrides", "dark-theme-overrides", "data":
|
||||
if f.Map() == nil {
|
||||
c.errorf(f.LastRef().AST(), `"%s" needs a map`, f.Name)
|
||||
c.errorf(f.LastRef().AST(), `"%s" needs a map`, f.Name.ScalarString())
|
||||
continue
|
||||
}
|
||||
case "theme-id", "dark-theme-id":
|
||||
valInt, err := strconv.Atoi(val)
|
||||
if err != nil {
|
||||
c.errorf(f.LastRef().AST(), `expected an integer for "%s", got "%s"`, f.Name, val)
|
||||
c.errorf(f.LastRef().AST(), `expected an integer for "%s", got "%s"`, f.Name.ScalarString(), val)
|
||||
continue
|
||||
}
|
||||
if d2themescatalog.Find(int64(valInt)) == (d2themes.Theme{}) {
|
||||
|
|
@ -209,12 +214,12 @@ func (c *compiler) validateConfigs(configs *Field) {
|
|||
case "pad":
|
||||
_, err := strconv.Atoi(val)
|
||||
if err != nil {
|
||||
c.errorf(f.LastRef().AST(), `expected an integer for "%s", got "%s"`, f.Name, val)
|
||||
c.errorf(f.LastRef().AST(), `expected an integer for "%s", got "%s"`, f.Name.ScalarString(), val)
|
||||
continue
|
||||
}
|
||||
case "layout-engine":
|
||||
default:
|
||||
c.errorf(f.LastRef().AST(), `"%s" is not a valid config`, f.Name)
|
||||
c.errorf(f.LastRef().AST(), `"%s" is not a valid config`, f.Name.ScalarString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -227,8 +232,8 @@ func (c *compiler) resolveSubstitutions(varsStack []*Map, node Node) (removedFie
|
|||
case *d2ast.UnquotedString:
|
||||
for i, box := range s.Value {
|
||||
if box.Substitution != nil {
|
||||
for _, vars := range varsStack {
|
||||
resolvedField = c.resolveSubstitution(vars, box.Substitution)
|
||||
for i, vars := range varsStack {
|
||||
resolvedField = c.resolveSubstitution(vars, node, box.Substitution, i == 0)
|
||||
if resolvedField != nil {
|
||||
if resolvedField.Primary() != nil {
|
||||
if _, ok := resolvedField.Primary().Value.(*d2ast.Null); ok {
|
||||
|
|
@ -262,11 +267,11 @@ func (c *compiler) resolveSubstitutions(varsStack []*Map, node Node) (removedFie
|
|||
}
|
||||
}
|
||||
case *Field:
|
||||
m := ParentMap(n)
|
||||
if resolvedField.Map() != nil {
|
||||
OverlayMap(ParentMap(n), resolvedField.Map())
|
||||
ExpandSubstitution(m, resolvedField.Map(), n)
|
||||
}
|
||||
// Remove the placeholder field
|
||||
m := n.parent.(*Map)
|
||||
for i, f2 := range m.Fields {
|
||||
if n == f2 {
|
||||
m.Fields = append(m.Fields[:i], m.Fields[i+1:]...)
|
||||
|
|
@ -319,8 +324,8 @@ func (c *compiler) resolveSubstitutions(varsStack []*Map, node Node) (removedFie
|
|||
case *d2ast.DoubleQuotedString:
|
||||
for i, box := range s.Value {
|
||||
if box.Substitution != nil {
|
||||
for _, vars := range varsStack {
|
||||
resolvedField = c.resolveSubstitution(vars, box.Substitution)
|
||||
for i, vars := range varsStack {
|
||||
resolvedField = c.resolveSubstitution(vars, node, box.Substitution, i == 0)
|
||||
if resolvedField != nil {
|
||||
break
|
||||
}
|
||||
|
|
@ -340,20 +345,64 @@ func (c *compiler) resolveSubstitutions(varsStack []*Map, node Node) (removedFie
|
|||
if subbed {
|
||||
s.Coalesce()
|
||||
}
|
||||
case *d2ast.BlockString:
|
||||
variables := make(map[string]string)
|
||||
for _, vars := range varsStack {
|
||||
c.collectVariables(vars, variables)
|
||||
}
|
||||
preprocessedValue := textmeasure.ReplaceSubstitutionsMarkdown(s.Value, variables)
|
||||
|
||||
// Update the block string value
|
||||
s.Value = preprocessedValue
|
||||
}
|
||||
return removedField
|
||||
}
|
||||
|
||||
func (c *compiler) resolveSubstitution(vars *Map, substitution *d2ast.Substitution) *Field {
|
||||
func (c *compiler) collectVariables(vars *Map, variables map[string]string) {
|
||||
if vars == nil {
|
||||
return
|
||||
}
|
||||
for _, f := range vars.Fields {
|
||||
if f.Primary() != nil {
|
||||
variables[f.Name.ScalarString()] = f.Primary().Value.ScalarString()
|
||||
} else if f.Map() != nil {
|
||||
c.collectVariables(f.Map(), variables)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *compiler) resolveSubstitution(vars *Map, node Node, substitution *d2ast.Substitution, isCurrentScopeVars bool) *Field {
|
||||
if vars == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
fieldNode, fok := node.(*Field)
|
||||
parent := ParentField(node)
|
||||
|
||||
for i, p := range substitution.Path {
|
||||
f := vars.GetField(p.Unbox().ScalarString())
|
||||
f := vars.GetField(p.Unbox())
|
||||
if f == nil {
|
||||
return nil
|
||||
}
|
||||
// Consider this case:
|
||||
//
|
||||
// ```
|
||||
// vars: {
|
||||
// x: a
|
||||
// }
|
||||
// hi: {
|
||||
// vars: {
|
||||
// x: ${x}-b
|
||||
// }
|
||||
// yo: ${x}
|
||||
// }
|
||||
// ```
|
||||
//
|
||||
// When resolving hi.vars.x, the vars stack includes itself.
|
||||
// So this next if clause says, "ignore if we're using the current scope's vars to try to resolve a substitution that requires a var from further in the stack"
|
||||
if fok && fieldNode.Name != nil && fieldNode.Name.ScalarString() == p.Unbox().ScalarString() && isCurrentScopeVars && parent.Name.ScalarString() == "vars" && parent.Name.IsUnquoted() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if i == len(substitution.Path)-1 {
|
||||
return f
|
||||
|
|
@ -382,17 +431,15 @@ func (g *globContext) copy() *globContext {
|
|||
return &g2
|
||||
}
|
||||
|
||||
func (g *globContext) prefixed(dst *Map) *globContext {
|
||||
g2 := g.copy()
|
||||
prefix := d2ast.MakeKeyPath(RelIDA(g2.refctx.ScopeMap, dst))
|
||||
g2.refctx.Key = g2.refctx.Key.Copy()
|
||||
if g2.refctx.Key.Key != nil {
|
||||
prefix.Path = append(prefix.Path, g2.refctx.Key.Key.Path...)
|
||||
func (g *globContext) copyApplied(from *globContext) {
|
||||
g.appliedFields = make(map[string]struct{})
|
||||
for k, v := range from.appliedFields {
|
||||
g.appliedFields[k] = v
|
||||
}
|
||||
if len(prefix.Path) > 0 {
|
||||
g2.refctx.Key.Key = prefix
|
||||
g.appliedEdges = make(map[string]struct{})
|
||||
for k, v := range from.appliedEdges {
|
||||
g.appliedEdges[k] = v
|
||||
}
|
||||
return g2
|
||||
}
|
||||
|
||||
func (c *compiler) ampersandFilterMap(dst *Map, ast, scopeAST *d2ast.Map) bool {
|
||||
|
|
@ -405,6 +452,9 @@ func (c *compiler) ampersandFilterMap(dst *Map, ast, scopeAST *d2ast.Map) bool {
|
|||
ScopeMap: dst,
|
||||
ScopeAST: scopeAST,
|
||||
})
|
||||
if n.MapKey.NotAmpersand {
|
||||
ok = !ok
|
||||
}
|
||||
if !ok {
|
||||
if len(c.mapRefContextStack) == 0 {
|
||||
return false
|
||||
|
|
@ -415,10 +465,10 @@ func (c *compiler) ampersandFilterMap(dst *Map, ast, scopeAST *d2ast.Map) bool {
|
|||
return false
|
||||
}
|
||||
var ks string
|
||||
if gctx.refctx.Key.HasTripleGlob() {
|
||||
ks = d2format.Format(d2ast.MakeKeyPath(IDA(dst)))
|
||||
if gctx.refctx.Key.HasMultiGlob() {
|
||||
ks = d2format.Format(d2ast.MakeKeyPathString(IDA(dst)))
|
||||
} else {
|
||||
ks = d2format.Format(d2ast.MakeKeyPath(BoardIDA(dst)))
|
||||
ks = d2format.Format(d2ast.MakeKeyPathString(BoardIDA(dst)))
|
||||
}
|
||||
delete(gctx.appliedFields, ks)
|
||||
delete(gctx.appliedEdges, ks)
|
||||
|
|
@ -438,13 +488,38 @@ func (c *compiler) compileMap(dst *Map, ast, scopeAST *d2ast.Map) {
|
|||
if NodeBoardKind(dst) == BoardLayer && !dst.Root() {
|
||||
for _, g := range previousGlobs {
|
||||
if g.refctx.Key.HasTripleGlob() {
|
||||
globs = append(globs, g.prefixed(dst))
|
||||
gctx2 := g.copy()
|
||||
gctx2.refctx.ScopeMap = dst
|
||||
globs = append(globs, gctx2)
|
||||
}
|
||||
}
|
||||
} else if NodeBoardKind(dst) != "" {
|
||||
// Make all globs relative to the scenario or step.
|
||||
} else if NodeBoardKind(dst) == BoardScenario {
|
||||
for _, g := range previousGlobs {
|
||||
globs = append(globs, g.prefixed(dst))
|
||||
gctx2 := g.copy()
|
||||
gctx2.refctx.ScopeMap = dst
|
||||
if !g.refctx.Key.HasMultiGlob() {
|
||||
// Triple globs already apply independently to each board
|
||||
gctx2.copyApplied(g)
|
||||
}
|
||||
globs = append(globs, gctx2)
|
||||
}
|
||||
for _, g := range previousGlobs {
|
||||
g2 := g.copy()
|
||||
g2.refctx.ScopeMap = dst
|
||||
// We don't want globs applied in a given scenario to affect future boards
|
||||
// Copying the applied fields and edges keeps the applications scoped to this board
|
||||
// Note that this is different from steps, where applications carry over
|
||||
if !g.refctx.Key.HasMultiGlob() {
|
||||
// Triple globs already apply independently to each board
|
||||
g2.copyApplied(g)
|
||||
}
|
||||
globs = append(globs, g2)
|
||||
}
|
||||
} else if NodeBoardKind(dst) == BoardStep {
|
||||
for _, g := range previousGlobs {
|
||||
gctx2 := g.copy()
|
||||
gctx2.refctx.ScopeMap = dst
|
||||
globs = append(globs, gctx2)
|
||||
}
|
||||
} else {
|
||||
globs = append(globs, previousGlobs...)
|
||||
|
|
@ -489,6 +564,7 @@ func (c *compiler) compileMap(dst *Map, ast, scopeAST *d2ast.Map) {
|
|||
}
|
||||
dst.Fields = append(dst.Fields, f)
|
||||
case n.Import != nil:
|
||||
// Spread import
|
||||
impn, ok := c._import(n.Import)
|
||||
if !ok {
|
||||
continue
|
||||
|
|
@ -497,6 +573,7 @@ func (c *compiler) compileMap(dst *Map, ast, scopeAST *d2ast.Map) {
|
|||
c.errorf(n.Import, "cannot spread import non map into map")
|
||||
continue
|
||||
}
|
||||
impn.(Importable).SetImportAST(n.Import)
|
||||
|
||||
for _, gctx := range impn.Map().globs {
|
||||
if !gctx.refctx.Key.HasTripleGlob() {
|
||||
|
|
@ -509,6 +586,8 @@ func (c *compiler) compileMap(dst *Map, ast, scopeAST *d2ast.Map) {
|
|||
}
|
||||
|
||||
OverlayMap(dst, impn.Map())
|
||||
impDir := n.Import.Dir()
|
||||
c.extendLinks(dst, ParentField(dst), impDir)
|
||||
|
||||
if impnf, ok := impn.(*Field); ok {
|
||||
if impnf.Primary_ != nil {
|
||||
|
|
@ -587,7 +666,7 @@ func (c *compiler) compileKey(refctx *RefContext) {
|
|||
}
|
||||
|
||||
func (c *compiler) compileField(dst *Map, kp *d2ast.KeyPath, refctx *RefContext) {
|
||||
if refctx.Key.Ampersand {
|
||||
if refctx.Key.Ampersand || refctx.Key.NotAmpersand {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -603,7 +682,7 @@ func (c *compiler) compileField(dst *Map, kp *d2ast.KeyPath, refctx *RefContext)
|
|||
}
|
||||
|
||||
func (c *compiler) ampersandFilter(refctx *RefContext) bool {
|
||||
if !refctx.Key.Ampersand {
|
||||
if !refctx.Key.Ampersand && !refctx.Key.NotAmpersand {
|
||||
return true
|
||||
}
|
||||
if len(c.mapRefContextStack) == 0 || !c.mapRefContextStack[len(c.mapRefContextStack)-1].Key.SupportsGlobFilters() {
|
||||
|
|
@ -620,44 +699,105 @@ func (c *compiler) ampersandFilter(refctx *RefContext) bool {
|
|||
return false
|
||||
}
|
||||
if len(fa) == 0 {
|
||||
if refctx.Key.Key.Last().ScalarString() != "label" {
|
||||
if refctx.Key.Value.ScalarBox().Unbox().ScalarString() == "*" {
|
||||
return false
|
||||
}
|
||||
kp := refctx.Key.Key.Copy()
|
||||
kp.Path = kp.Path[:len(kp.Path)-1]
|
||||
if len(kp.Path) == 0 {
|
||||
n := refctx.ScopeMap.Parent()
|
||||
switch n := n.(type) {
|
||||
case *Field:
|
||||
fa = append(fa, n)
|
||||
case *Edge:
|
||||
if n.Primary_ == nil {
|
||||
if refctx.Key.Value.ScalarBox().Unbox().ScalarString() == "" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
if n.Primary_.Value.ScalarString() != refctx.Key.Value.ScalarBox().Unbox().ScalarString() {
|
||||
return false
|
||||
}
|
||||
// The field/edge has no value for this filter
|
||||
// But the filter might still match default, e.g. opacity 1
|
||||
// So we make a fake field for the default
|
||||
// NOTE: this does not apply to things that themes control, like stroke and fill
|
||||
// Nor does it apply to layout things like width and height
|
||||
switch refctx.Key.Key.Last().ScalarString() {
|
||||
case "shape":
|
||||
f := &Field{
|
||||
Primary_: &Scalar{
|
||||
Value: d2ast.FlatUnquotedString("rectangle"),
|
||||
},
|
||||
}
|
||||
} else {
|
||||
fa, err = refctx.ScopeMap.EnsureField(kp, refctx, false, c)
|
||||
return c._ampersandFilter(f, refctx)
|
||||
case "border-radius", "stroke-dash":
|
||||
f := &Field{
|
||||
Primary_: &Scalar{
|
||||
Value: d2ast.FlatUnquotedString("0"),
|
||||
},
|
||||
}
|
||||
return c._ampersandFilter(f, refctx)
|
||||
case "opacity":
|
||||
f := &Field{
|
||||
Primary_: &Scalar{
|
||||
Value: d2ast.FlatUnquotedString("1"),
|
||||
},
|
||||
}
|
||||
return c._ampersandFilter(f, refctx)
|
||||
case "stroke-width":
|
||||
f := &Field{
|
||||
Primary_: &Scalar{
|
||||
Value: d2ast.FlatUnquotedString("2"),
|
||||
},
|
||||
}
|
||||
return c._ampersandFilter(f, refctx)
|
||||
case "icon", "tooltip", "link":
|
||||
f := &Field{
|
||||
Primary_: &Scalar{
|
||||
Value: d2ast.FlatUnquotedString(""),
|
||||
},
|
||||
}
|
||||
return c._ampersandFilter(f, refctx)
|
||||
case "shadow", "multiple", "3d", "animated", "filled":
|
||||
f := &Field{
|
||||
Primary_: &Scalar{
|
||||
Value: d2ast.FlatUnquotedString("false"),
|
||||
},
|
||||
}
|
||||
return c._ampersandFilter(f, refctx)
|
||||
case "leaf":
|
||||
raw := refctx.Key.Value.ScalarBox().Unbox().ScalarString()
|
||||
boolVal, err := strconv.ParseBool(raw)
|
||||
if err != nil {
|
||||
c.err.Errors = append(c.err.Errors, err.(d2ast.Error))
|
||||
c.errorf(refctx.Key, `&leaf must be "true" or "false", got %q`, raw)
|
||||
return false
|
||||
}
|
||||
}
|
||||
for _, f := range fa {
|
||||
label := f.Name
|
||||
if f.Primary_ != nil {
|
||||
label = f.Primary_.Value.ScalarString()
|
||||
}
|
||||
if label != refctx.Key.Value.ScalarBox().Unbox().ScalarString() {
|
||||
|
||||
f := refctx.ScopeMap.Parent().(*Field)
|
||||
isLeaf := f.Map() == nil || !f.Map().IsContainer()
|
||||
return isLeaf == boolVal
|
||||
case "connected":
|
||||
raw := refctx.Key.Value.ScalarBox().Unbox().ScalarString()
|
||||
boolVal, err := strconv.ParseBool(raw)
|
||||
if err != nil {
|
||||
c.errorf(refctx.Key, `&connected must be "true" or "false", got %q`, raw)
|
||||
return false
|
||||
}
|
||||
f := refctx.ScopeMap.Parent().(*Field)
|
||||
isConnected := false
|
||||
for _, r := range f.References {
|
||||
if r.InEdge() {
|
||||
isConnected = true
|
||||
break
|
||||
}
|
||||
}
|
||||
return isConnected == boolVal
|
||||
case "label":
|
||||
f := &Field{}
|
||||
n := refctx.ScopeMap.Parent()
|
||||
if n.Primary() == nil {
|
||||
switch n := n.(type) {
|
||||
case *Field:
|
||||
// The label value for fields is their key value
|
||||
f.Primary_ = &Scalar{
|
||||
Value: n.Name,
|
||||
}
|
||||
case *Edge:
|
||||
// But for edges, it's nothing
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
f.Primary_ = n.Primary()
|
||||
}
|
||||
return c._ampersandFilter(f, refctx)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
for _, f := range fa {
|
||||
ok := c._ampersandFilter(f, refctx)
|
||||
|
|
@ -688,8 +828,14 @@ func (c *compiler) _ampersandFilter(f *Field, refctx *RefContext) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
if refctx.Key.Value.ScalarBox().Unbox().ScalarString() != f.Primary_.Value.ScalarString() {
|
||||
return false
|
||||
us, ok := refctx.Key.Value.ScalarBox().Unbox().(*d2ast.UnquotedString)
|
||||
|
||||
if ok && us.Pattern != nil {
|
||||
return matchPattern(f.Primary_.Value.ScalarString(), us.Pattern)
|
||||
} else {
|
||||
if refctx.Key.Value.ScalarBox().Unbox().ScalarString() != f.Primary_.Value.ScalarString() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
|
|
@ -715,7 +861,7 @@ func (c *compiler) _compileField(f *Field, refctx *RefContext) {
|
|||
// For vars, if we delete the field, it may just resolve to an outer scope var of the same name
|
||||
// Instead we keep it around, so that resolveSubstitutions can find it
|
||||
if !IsVar(ParentMap(f)) {
|
||||
ParentMap(f).DeleteField(f.Name)
|
||||
ParentMap(f).DeleteField(f.Name.ScalarString())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
@ -773,10 +919,12 @@ func (c *compiler) _compileField(f *Field, refctx *RefContext) {
|
|||
c.overlayClasses(f.Map())
|
||||
}
|
||||
} else if refctx.Key.Value.Import != nil {
|
||||
// Non-spread import
|
||||
n, ok := c._import(refctx.Key.Value.Import)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
n.(Importable).SetImportAST(refctx.Key.Value.Import)
|
||||
switch n := n.(type) {
|
||||
case *Field:
|
||||
if n.Primary_ != nil {
|
||||
|
|
@ -806,7 +954,8 @@ func (c *compiler) _compileField(f *Field, refctx *RefContext) {
|
|||
}
|
||||
}
|
||||
OverlayMap(f.Map(), n)
|
||||
c.updateLinks(f.Map())
|
||||
impDir := refctx.Key.Value.Import.Dir()
|
||||
c.extendLinks(f.Map(), f, impDir)
|
||||
switch NodeBoardKind(f) {
|
||||
case BoardScenario, BoardStep:
|
||||
c.overlayClasses(f.Map())
|
||||
|
|
@ -821,7 +970,7 @@ func (c *compiler) _compileField(f *Field, refctx *RefContext) {
|
|||
Value: refctx.Key.Value.ScalarBox().Unbox(),
|
||||
}
|
||||
// If the link is a board, we need to transform it into an absolute path.
|
||||
if f.Name == "link" {
|
||||
if f.Name.ScalarString() == "link" && f.Name.IsUnquoted() {
|
||||
c.compileLink(f, refctx)
|
||||
}
|
||||
}
|
||||
|
|
@ -839,62 +988,66 @@ func (c *compiler) ignoreLazyGlob(n Node) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
func (c *compiler) updateLinks(m *Map) {
|
||||
// When importing a file, all of its board and icon links need to be extended to reflect their new path
|
||||
func (c *compiler) extendLinks(m *Map, importF *Field, importDir string) {
|
||||
nodeBoardKind := NodeBoardKind(m)
|
||||
importIDA := IDA(importF)
|
||||
for _, f := range m.Fields {
|
||||
if f.Name == "link" {
|
||||
if f.Name.ScalarString() == "link" && f.Name.IsUnquoted() {
|
||||
if nodeBoardKind != "" {
|
||||
c.errorf(f.LastRef().AST(), "a board itself cannot be linked; only objects within a board can be linked")
|
||||
continue
|
||||
}
|
||||
val := f.Primary().Value.ScalarString()
|
||||
|
||||
u, err := url.Parse(html.UnescapeString(val))
|
||||
isRemote := err == nil && u.Scheme != ""
|
||||
if isRemote {
|
||||
continue
|
||||
}
|
||||
|
||||
link, err := d2parser.ParseKey(val)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
linkIDA := link.IDA()
|
||||
if len(linkIDA) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// When updateLinks is called, all valid board links are already compiled and changed to the qualified path beginning with "root"
|
||||
if linkIDA[0] != "root" {
|
||||
for _, id := range linkIDA[1:] {
|
||||
if id.ScalarString() == "_" && id.IsUnquoted() {
|
||||
if len(linkIDA) < 2 || len(importIDA) < 2 {
|
||||
break
|
||||
}
|
||||
linkIDA = append([]d2ast.String{linkIDA[0]}, linkIDA[2:]...)
|
||||
importIDA = importIDA[:len(importIDA)-2]
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
extendedIDA := append(importIDA, linkIDA[1:]...)
|
||||
kp := d2ast.MakeKeyPathString(extendedIDA)
|
||||
s := d2format.Format(kp)
|
||||
f.Primary_.Value = d2ast.MakeValueBox(d2ast.FlatUnquotedString(s)).ScalarBox().Unbox()
|
||||
}
|
||||
if f.Name.ScalarString() == "icon" && f.Name.IsUnquoted() && f.Primary() != nil {
|
||||
val := f.Primary().Value.ScalarString()
|
||||
// It's likely a substitution
|
||||
if val == "" {
|
||||
continue
|
||||
}
|
||||
bida := BoardIDA(f)
|
||||
aida := IDA(f)
|
||||
if len(bida) != len(aida) {
|
||||
prependIDA := aida[:len(aida)-len(bida)]
|
||||
fullIDA := []string{"root"}
|
||||
// With nested imports, a value may already have been updated with part of the absolute path
|
||||
// E.g.,
|
||||
// The import prepends path a b c
|
||||
// The existing path is b c d
|
||||
// So the new path is
|
||||
// a b c
|
||||
// b c d
|
||||
// -------
|
||||
// a b c d
|
||||
OUTER:
|
||||
for i := 1; i < len(prependIDA); i += 2 {
|
||||
for j := 0; i+j < len(prependIDA); j++ {
|
||||
if prependIDA[i+j] != linkIDA[1+j] {
|
||||
break
|
||||
}
|
||||
// Reached the end and all common
|
||||
if i+j == len(prependIDA)-1 {
|
||||
break OUTER
|
||||
}
|
||||
}
|
||||
fullIDA = append(fullIDA, prependIDA[i])
|
||||
fullIDA = append(fullIDA, prependIDA[i+1])
|
||||
}
|
||||
// Chop off "root"
|
||||
fullIDA = append(fullIDA, linkIDA[1:]...)
|
||||
|
||||
kp := d2ast.MakeKeyPath(fullIDA)
|
||||
s := d2format.Format(kp)
|
||||
f.Primary_.Value = d2ast.MakeValueBox(d2ast.FlatUnquotedString(s)).ScalarBox().Unbox()
|
||||
u, err := url.Parse(html.UnescapeString(val))
|
||||
isRemoteImg := err == nil && u.Scheme != ""
|
||||
if isRemoteImg {
|
||||
continue
|
||||
}
|
||||
val = path.Join(importDir, val)
|
||||
f.Primary_.Value = d2ast.MakeValueBox(d2ast.FlatUnquotedString(val)).ScalarBox().Unbox()
|
||||
}
|
||||
if f.Map() != nil {
|
||||
c.updateLinks(f.Map())
|
||||
c.extendLinks(f.Map(), importF, importDir)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -917,47 +1070,50 @@ func (c *compiler) compileLink(f *Field, refctx *RefContext) {
|
|||
return
|
||||
}
|
||||
|
||||
if linkIDA[0] == "root" {
|
||||
if linkIDA[0].ScalarString() == "root" && linkIDA[0].IsUnquoted() {
|
||||
c.errorf(refctx.Key.Key, "cannot refer to root in link")
|
||||
return
|
||||
}
|
||||
|
||||
if !linkIDA[0].IsUnquoted() {
|
||||
return
|
||||
}
|
||||
|
||||
// If it doesn't start with one of these reserved words, the link is definitely not a board link.
|
||||
if !strings.EqualFold(linkIDA[0], "layers") && !strings.EqualFold(linkIDA[0], "scenarios") && !strings.EqualFold(linkIDA[0], "steps") && linkIDA[0] != "_" {
|
||||
if !strings.EqualFold(linkIDA[0].ScalarString(), "layers") && !strings.EqualFold(linkIDA[0].ScalarString(), "scenarios") && !strings.EqualFold(linkIDA[0].ScalarString(), "steps") && linkIDA[0].ScalarString() != "_" {
|
||||
return
|
||||
}
|
||||
|
||||
// Chop off the non-board portion of the scope, like if this is being defined on a nested object (e.g. `x.y.z`)
|
||||
for i := len(scopeIDA) - 1; i > 0; i-- {
|
||||
if strings.EqualFold(scopeIDA[i-1], "layers") || strings.EqualFold(scopeIDA[i-1], "scenarios") || strings.EqualFold(scopeIDA[i-1], "steps") {
|
||||
if scopeIDA[i-1].IsUnquoted() && (strings.EqualFold(scopeIDA[i-1].ScalarString(), "layers") || strings.EqualFold(scopeIDA[i-1].ScalarString(), "scenarios") || strings.EqualFold(scopeIDA[i-1].ScalarString(), "steps")) {
|
||||
scopeIDA = scopeIDA[:i+1]
|
||||
break
|
||||
}
|
||||
if scopeIDA[i-1] == "root" {
|
||||
if scopeIDA[i-1].ScalarString() == "root" && scopeIDA[i-1].IsUnquoted() {
|
||||
scopeIDA = scopeIDA[:i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve underscores
|
||||
for len(linkIDA) > 0 && linkIDA[0] == "_" {
|
||||
for len(linkIDA) > 0 && linkIDA[0].ScalarString() == "_" && linkIDA[0].IsUnquoted() {
|
||||
if len(scopeIDA) < 2 {
|
||||
// IR compiler only validates bad underscore usage
|
||||
// The compiler will validate if the target board actually exists
|
||||
c.errorf(refctx.Key.Key, "invalid underscore usage")
|
||||
return
|
||||
// Leave the underscore. It will fail in compiler as a standalone board,
|
||||
// but if imported, will get further resolved in extendLinks
|
||||
break
|
||||
}
|
||||
// pop 2 off path per one underscore
|
||||
scopeIDA = scopeIDA[:len(scopeIDA)-2]
|
||||
linkIDA = linkIDA[1:]
|
||||
}
|
||||
if len(scopeIDA) == 0 {
|
||||
scopeIDA = []string{"root"}
|
||||
scopeIDA = []d2ast.String{d2ast.FlatUnquotedString("root")}
|
||||
}
|
||||
|
||||
// Create the absolute path by appending scope path with value specified
|
||||
scopeIDA = append(scopeIDA, linkIDA...)
|
||||
kp := d2ast.MakeKeyPath(scopeIDA)
|
||||
kp := d2ast.MakeKeyPathString(scopeIDA)
|
||||
f.Primary_.Value = d2ast.FlatUnquotedString(d2format.Format(kp))
|
||||
}
|
||||
|
||||
|
|
@ -991,7 +1147,7 @@ func (c *compiler) compileEdges(refctx *RefContext) {
|
|||
func (c *compiler) _compileEdges(refctx *RefContext) {
|
||||
eida := NewEdgeIDs(refctx.Key)
|
||||
for i, eid := range eida {
|
||||
if refctx.Key != nil && refctx.Key.Value.Null != nil {
|
||||
if !eid.Glob && (refctx.Key.Primary.Null != nil || refctx.Key.Value.Null != nil) {
|
||||
refctx.ScopeMap.DeleteEdge(eid)
|
||||
continue
|
||||
}
|
||||
|
|
@ -1009,6 +1165,10 @@ func (c *compiler) _compileEdges(refctx *RefContext) {
|
|||
continue
|
||||
}
|
||||
for _, e := range ea {
|
||||
if refctx.Key.Primary.Null != nil || refctx.Key.Value.Null != nil {
|
||||
refctx.ScopeMap.DeleteEdge(e.ID)
|
||||
continue
|
||||
}
|
||||
e.References = append(e.References, &EdgeReference{
|
||||
Context_: refctx,
|
||||
DueToGlob_: len(c.globRefContextStack) > 0,
|
||||
|
|
@ -1096,6 +1256,7 @@ func (c *compiler) compileArray(dst *Array, a *d2ast.Array, scopeAST *d2ast.Map)
|
|||
if !ok {
|
||||
continue
|
||||
}
|
||||
n.(Importable).SetImportAST(v)
|
||||
switch n := n.(type) {
|
||||
case *Field:
|
||||
if v.Spread {
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ func TestCompile(t *testing.T) {
|
|||
t.Run("imports", testCompileImports)
|
||||
t.Run("patterns", testCompilePatterns)
|
||||
t.Run("filters", testCompileFilters)
|
||||
t.Run("vars", testCompileVars)
|
||||
}
|
||||
|
||||
type testCase struct {
|
||||
|
|
@ -194,6 +195,23 @@ func testCompileFields(t *testing.T) {
|
|||
assert.String(t, `[1; 2; 3; 4]`, f.Composite.String())
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "quoted",
|
||||
run: func(t testing.TB) {
|
||||
m, err := compile(t, `my_table: {
|
||||
shape: sql_table
|
||||
width: 200
|
||||
height: 200
|
||||
"shape": string
|
||||
"icon": string
|
||||
"width": int
|
||||
"height": int
|
||||
}`)
|
||||
assert.Success(t, err)
|
||||
assertQuery(t, m, 0, 0, "sql_table", "my_table.shape")
|
||||
assertQuery(t, m, 0, 0, "string", `my_table."shape"`)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "null",
|
||||
run: func(t testing.TB) {
|
||||
|
|
@ -698,3 +716,32 @@ layers: {
|
|||
}
|
||||
runa(t, tca)
|
||||
}
|
||||
|
||||
func testCompileVars(t *testing.T) {
|
||||
t.Parallel()
|
||||
tca := []testCase{
|
||||
{
|
||||
name: "spread-in-place",
|
||||
run: func(t testing.TB) {
|
||||
m, err := compile(t, `vars: {
|
||||
person-shape: {
|
||||
grid-columns: 1
|
||||
grid-rows: 2
|
||||
grid-gap: 0
|
||||
head
|
||||
body
|
||||
}
|
||||
}
|
||||
|
||||
dora: {
|
||||
...${person-shape}
|
||||
body
|
||||
}
|
||||
`)
|
||||
assert.Success(t, err)
|
||||
assert.Equal(t, "grid-columns", m.Fields[1].Map().Fields[0].Name.ScalarString())
|
||||
},
|
||||
},
|
||||
}
|
||||
runa(t, tca)
|
||||
}
|
||||
|
|
|
|||
385
d2ir/d2ir.go
385
d2ir/d2ir.go
|
|
@ -11,7 +11,6 @@ import (
|
|||
|
||||
"oss.terrastruct.com/d2/d2ast"
|
||||
"oss.terrastruct.com/d2/d2format"
|
||||
"oss.terrastruct.com/d2/d2graph"
|
||||
"oss.terrastruct.com/d2/d2parser"
|
||||
"oss.terrastruct.com/d2/d2target"
|
||||
)
|
||||
|
|
@ -167,16 +166,17 @@ func (s *Scalar) Equal(n2 Node) bool {
|
|||
}
|
||||
|
||||
type Map struct {
|
||||
parent Node
|
||||
Fields []*Field `json:"fields"`
|
||||
Edges []*Edge `json:"edges"`
|
||||
parent Node
|
||||
importAST d2ast.Node
|
||||
Fields []*Field `json:"fields"`
|
||||
Edges []*Edge `json:"edges"`
|
||||
|
||||
globs []*globContext
|
||||
}
|
||||
|
||||
func (m *Map) initRoot() {
|
||||
m.parent = &Field{
|
||||
Name: "root",
|
||||
Name: d2ast.FlatUnquotedString("root"),
|
||||
References: []*FieldReference{{
|
||||
Context_: &RefContext{
|
||||
ScopeMap: m,
|
||||
|
|
@ -185,6 +185,20 @@ func (m *Map) initRoot() {
|
|||
}
|
||||
}
|
||||
|
||||
func (m *Map) ImportAST() d2ast.Node {
|
||||
return m.importAST
|
||||
}
|
||||
|
||||
func (m *Map) SetImportAST(node d2ast.Node) {
|
||||
m.importAST = node
|
||||
for _, f := range m.Fields {
|
||||
f.SetImportAST(node)
|
||||
}
|
||||
for _, e := range m.Edges {
|
||||
e.SetImportAST(node)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Map) Copy(newParent Node) Node {
|
||||
tmp := *m
|
||||
m = &tmp
|
||||
|
|
@ -263,6 +277,9 @@ func NodeBoardKind(n Node) BoardKind {
|
|||
}
|
||||
f = ParentField(n)
|
||||
case *Map:
|
||||
if n == nil {
|
||||
return ""
|
||||
}
|
||||
var ok bool
|
||||
f, ok = n.parent.(*Field)
|
||||
if !ok {
|
||||
|
|
@ -276,7 +293,7 @@ func NodeBoardKind(n Node) BoardKind {
|
|||
if f == nil {
|
||||
return ""
|
||||
}
|
||||
switch f.Name {
|
||||
switch f.Name.ScalarString() {
|
||||
case "layers":
|
||||
return BoardLayer
|
||||
case "scenarios":
|
||||
|
|
@ -288,11 +305,21 @@ func NodeBoardKind(n Node) BoardKind {
|
|||
}
|
||||
}
|
||||
|
||||
type Importable interface {
|
||||
ImportAST() d2ast.Node
|
||||
SetImportAST(d2ast.Node)
|
||||
}
|
||||
|
||||
var _ Importable = &Edge{}
|
||||
var _ Importable = &Field{}
|
||||
var _ Importable = &Map{}
|
||||
|
||||
type Field struct {
|
||||
// *Map.
|
||||
parent Node
|
||||
parent Node
|
||||
importAST d2ast.Node
|
||||
|
||||
Name string `json:"name"`
|
||||
Name d2ast.String `json:"name"`
|
||||
|
||||
// Primary_ to avoid clashing with Primary(). We need to keep it exported for
|
||||
// encoding/json to marshal it so cannot prefix _ instead.
|
||||
|
|
@ -302,6 +329,17 @@ type Field struct {
|
|||
References []*FieldReference `json:"references,omitempty"`
|
||||
}
|
||||
|
||||
func (f *Field) ImportAST() d2ast.Node {
|
||||
return f.importAST
|
||||
}
|
||||
|
||||
func (f *Field) SetImportAST(node d2ast.Node) {
|
||||
f.importAST = node
|
||||
if f.Map() != nil {
|
||||
f.Map().SetImportAST(node)
|
||||
}
|
||||
}
|
||||
|
||||
func (f *Field) Copy(newParent Node) Node {
|
||||
tmp := *f
|
||||
f = &tmp
|
||||
|
|
@ -339,11 +377,11 @@ func (f *Field) LastRef() Reference {
|
|||
}
|
||||
|
||||
type EdgeID struct {
|
||||
SrcPath []string `json:"src_path"`
|
||||
SrcArrow bool `json:"src_arrow"`
|
||||
SrcPath []d2ast.String `json:"src_path"`
|
||||
SrcArrow bool `json:"src_arrow"`
|
||||
|
||||
DstPath []string `json:"dst_path"`
|
||||
DstArrow bool `json:"dst_arrow"`
|
||||
DstPath []d2ast.String `json:"dst_path"`
|
||||
DstArrow bool `json:"dst_arrow"`
|
||||
|
||||
// If nil, then any EdgeID with equal src/dst/arrows matches.
|
||||
Index *int `json:"index"`
|
||||
|
|
@ -371,8 +409,8 @@ func (eid *EdgeID) Copy() *EdgeID {
|
|||
tmp := *eid
|
||||
eid = &tmp
|
||||
|
||||
eid.SrcPath = append([]string(nil), eid.SrcPath...)
|
||||
eid.DstPath = append([]string(nil), eid.DstPath...)
|
||||
eid.SrcPath = append([]d2ast.String(nil), eid.SrcPath...)
|
||||
eid.DstPath = append([]d2ast.String(nil), eid.DstPath...)
|
||||
return eid
|
||||
}
|
||||
|
||||
|
|
@ -390,7 +428,7 @@ func (eid *EdgeID) Match(eid2 *EdgeID) bool {
|
|||
return false
|
||||
}
|
||||
for i, s := range eid.SrcPath {
|
||||
if !strings.EqualFold(s, eid2.SrcPath[i]) {
|
||||
if !strings.EqualFold(s.ScalarString(), eid2.SrcPath[i].ScalarString()) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
@ -402,7 +440,7 @@ func (eid *EdgeID) Match(eid2 *EdgeID) bool {
|
|||
return false
|
||||
}
|
||||
for i, s := range eid.DstPath {
|
||||
if !strings.EqualFold(s, eid2.DstPath[i]) {
|
||||
if !strings.EqualFold(s.ScalarString(), eid2.DstPath[i].ScalarString()) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
@ -412,21 +450,21 @@ func (eid *EdgeID) Match(eid2 *EdgeID) bool {
|
|||
|
||||
// resolve resolves both underscores and commons in eid.
|
||||
// It returns the new eid, containing map adjusted for underscores and common ida.
|
||||
func (eid *EdgeID) resolve(m *Map) (_ *EdgeID, _ *Map, common []string, _ error) {
|
||||
func (eid *EdgeID) resolve(m *Map) (_ *EdgeID, _ *Map, common []d2ast.String, _ error) {
|
||||
eid = eid.Copy()
|
||||
maxUnderscores := go2.Max(countUnderscores(eid.SrcPath), countUnderscores(eid.DstPath))
|
||||
for i := 0; i < maxUnderscores; i++ {
|
||||
if eid.SrcPath[0] == "_" {
|
||||
if eid.SrcPath[0].ScalarString() == "_" && eid.SrcPath[0].IsUnquoted() {
|
||||
eid.SrcPath = eid.SrcPath[1:]
|
||||
} else {
|
||||
mf := ParentField(m)
|
||||
eid.SrcPath = append([]string{mf.Name}, eid.SrcPath...)
|
||||
eid.SrcPath = append([]d2ast.String{mf.Name}, eid.SrcPath...)
|
||||
}
|
||||
if eid.DstPath[0] == "_" {
|
||||
if eid.DstPath[0].ScalarString() == "_" && eid.DstPath[0].IsUnquoted() {
|
||||
eid.DstPath = eid.DstPath[1:]
|
||||
} else {
|
||||
mf := ParentField(m)
|
||||
eid.DstPath = append([]string{mf.Name}, eid.DstPath...)
|
||||
eid.DstPath = append([]d2ast.String{mf.Name}, eid.DstPath...)
|
||||
}
|
||||
m = ParentMap(m)
|
||||
if m == nil {
|
||||
|
|
@ -435,7 +473,7 @@ func (eid *EdgeID) resolve(m *Map) (_ *EdgeID, _ *Map, common []string, _ error)
|
|||
}
|
||||
|
||||
for len(eid.SrcPath) > 1 && len(eid.DstPath) > 1 {
|
||||
if !strings.EqualFold(eid.SrcPath[0], eid.DstPath[0]) || eid.SrcPath[0] == "*" {
|
||||
if !strings.EqualFold(eid.SrcPath[0].ScalarString(), eid.DstPath[0].ScalarString()) || strings.Contains(eid.SrcPath[0].ScalarString(), "*") {
|
||||
return eid, m, common, nil
|
||||
}
|
||||
common = append(common, eid.SrcPath[0])
|
||||
|
|
@ -448,7 +486,8 @@ func (eid *EdgeID) resolve(m *Map) (_ *EdgeID, _ *Map, common []string, _ error)
|
|||
|
||||
type Edge struct {
|
||||
// *Map
|
||||
parent Node
|
||||
parent Node
|
||||
importAST d2ast.Node
|
||||
|
||||
ID *EdgeID `json:"edge_id"`
|
||||
|
||||
|
|
@ -458,6 +497,17 @@ type Edge struct {
|
|||
References []*EdgeReference `json:"references,omitempty"`
|
||||
}
|
||||
|
||||
func (e *Edge) ImportAST() d2ast.Node {
|
||||
return e.importAST
|
||||
}
|
||||
|
||||
func (e *Edge) SetImportAST(node d2ast.Node) {
|
||||
e.importAST = node
|
||||
if e.Map() != nil {
|
||||
e.Map().SetImportAST(node)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Edge) Copy(newParent Node) Node {
|
||||
tmp := *e
|
||||
e = &tmp
|
||||
|
|
@ -624,8 +674,8 @@ func (m *Map) IsContainer() bool {
|
|||
return false
|
||||
}
|
||||
for _, f := range m.Fields {
|
||||
_, isReserved := d2graph.ReservedKeywords[f.Name]
|
||||
if !isReserved {
|
||||
_, isReserved := d2ast.ReservedKeywords[f.Name.ScalarString()]
|
||||
if !(isReserved && f.Name.IsUnquoted()) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
@ -652,9 +702,9 @@ func (m *Map) EdgeCountRecursive() int {
|
|||
|
||||
func (m *Map) GetClassMap(name string) *Map {
|
||||
root := RootMap(m)
|
||||
classes := root.Map().GetField("classes")
|
||||
classes := root.Map().GetField(d2ast.FlatUnquotedString("classes"))
|
||||
if classes != nil && classes.Map() != nil {
|
||||
class := classes.Map().GetField(name)
|
||||
class := classes.Map().GetField(d2ast.FlatUnquotedString(name))
|
||||
if class != nil && class.Map() != nil {
|
||||
return class.Map()
|
||||
}
|
||||
|
|
@ -662,8 +712,8 @@ func (m *Map) GetClassMap(name string) *Map {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (m *Map) GetField(ida ...string) *Field {
|
||||
for len(ida) > 0 && ida[0] == "_" {
|
||||
func (m *Map) GetField(ida ...d2ast.String) *Field {
|
||||
for len(ida) > 0 && ida[0].ScalarString() == "_" && ida[0].IsUnquoted() {
|
||||
m = ParentMap(m)
|
||||
if m == nil {
|
||||
return nil
|
||||
|
|
@ -672,7 +722,7 @@ func (m *Map) GetField(ida ...string) *Field {
|
|||
return m.getField(ida)
|
||||
}
|
||||
|
||||
func (m *Map) getField(ida []string) *Field {
|
||||
func (m *Map) getField(ida []d2ast.String) *Field {
|
||||
if len(ida) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -680,12 +730,18 @@ func (m *Map) getField(ida []string) *Field {
|
|||
s := ida[0]
|
||||
rest := ida[1:]
|
||||
|
||||
if s == "_" {
|
||||
if s.ScalarString() == "_" && s.IsUnquoted() {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, f := range m.Fields {
|
||||
if !strings.EqualFold(f.Name, s) {
|
||||
if f.Name == nil {
|
||||
continue
|
||||
}
|
||||
if !strings.EqualFold(f.Name.ScalarString(), s.ScalarString()) {
|
||||
continue
|
||||
}
|
||||
if f.Name.IsUnquoted() != s.IsUnquoted() {
|
||||
continue
|
||||
}
|
||||
if len(rest) == 0 {
|
||||
|
|
@ -701,7 +757,7 @@ func (m *Map) getField(ida []string) *Field {
|
|||
// EnsureField is a bit of a misnomer. It's more of a Query/Ensure combination function at this point.
|
||||
func (m *Map) EnsureField(kp *d2ast.KeyPath, refctx *RefContext, create bool, c *compiler) ([]*Field, error) {
|
||||
i := 0
|
||||
for kp.Path[i].Unbox().ScalarString() == "_" {
|
||||
for kp.Path[i].Unbox().ScalarString() == "_" && kp.Path[i].Unbox().IsUnquoted() {
|
||||
m = ParentMap(m)
|
||||
if m == nil {
|
||||
return nil, d2parser.Errorf(kp.Path[i].Unbox(), "invalid underscore: no parent")
|
||||
|
|
@ -734,10 +790,10 @@ func (m *Map) ensureField(i int, kp *d2ast.KeyPath, refctx *RefContext, create b
|
|||
filter := func(f *Field, passthrough bool) bool {
|
||||
if gctx != nil {
|
||||
var ks string
|
||||
if refctx.Key.HasTripleGlob() {
|
||||
ks = d2format.Format(d2ast.MakeKeyPath(IDA(f)))
|
||||
if refctx.Key.HasMultiGlob() {
|
||||
ks = d2format.Format(d2ast.MakeKeyPathString(IDA(f)))
|
||||
} else {
|
||||
ks = d2format.Format(d2ast.MakeKeyPath(BoardIDA(f)))
|
||||
ks = d2format.Format(d2ast.MakeKeyPathString(BoardIDA(f)))
|
||||
}
|
||||
if !kp.HasGlob() {
|
||||
if !passthrough {
|
||||
|
|
@ -791,7 +847,10 @@ func (m *Map) ensureField(i int, kp *d2ast.KeyPath, refctx *RefContext, create b
|
|||
return nil
|
||||
}
|
||||
for _, f := range m.Fields {
|
||||
if matchPattern(f.Name, us.Pattern) {
|
||||
if f.Name == nil {
|
||||
continue
|
||||
}
|
||||
if matchPattern(f.Name.ScalarString(), us.Pattern) {
|
||||
if i == len(kp.Path)-1 {
|
||||
faAppend(f)
|
||||
} else {
|
||||
|
|
@ -813,29 +872,30 @@ func (m *Map) ensureField(i int, kp *d2ast.KeyPath, refctx *RefContext, create b
|
|||
return nil
|
||||
}
|
||||
|
||||
head := kp.Path[i].Unbox().ScalarString()
|
||||
head := kp.Path[i].Unbox()
|
||||
headString := head.ScalarString()
|
||||
|
||||
if _, ok := d2graph.ReservedKeywords[strings.ToLower(head)]; ok {
|
||||
head = strings.ToLower(head)
|
||||
if _, ok := d2graph.CompositeReservedKeywords[head]; !ok && i < len(kp.Path)-1 {
|
||||
return d2parser.Errorf(kp.Path[i].Unbox(), fmt.Sprintf(`"%s" must be the last part of the key`, head))
|
||||
if _, ok := d2ast.ReservedKeywords[strings.ToLower(head.ScalarString())]; ok && head.IsUnquoted() {
|
||||
headString = strings.ToLower(head.ScalarString())
|
||||
if _, ok := d2ast.CompositeReservedKeywords[headString]; !ok && i < len(kp.Path)-1 {
|
||||
return d2parser.Errorf(kp.Path[i].Unbox(), fmt.Sprintf(`"%s" must be the last part of the key`, headString))
|
||||
}
|
||||
}
|
||||
|
||||
if head == "_" {
|
||||
if headString == "_" && head.IsUnquoted() {
|
||||
return d2parser.Errorf(kp.Path[i].Unbox(), `parent "_" can only be used in the beginning of paths, e.g. "_.x"`)
|
||||
}
|
||||
|
||||
if head == "classes" && NodeBoardKind(m) == "" {
|
||||
return d2parser.Errorf(kp.Path[i].Unbox(), "%s is only allowed at a board root", head)
|
||||
if headString == "classes" && head.IsUnquoted() && NodeBoardKind(m) == "" {
|
||||
return d2parser.Errorf(kp.Path[i].Unbox(), "%s is only allowed at a board root", headString)
|
||||
}
|
||||
|
||||
if findBoardKeyword(head) != -1 && NodeBoardKind(m) == "" {
|
||||
return d2parser.Errorf(kp.Path[i].Unbox(), "%s is only allowed at a board root", head)
|
||||
if findBoardKeyword(head) != -1 && head.IsUnquoted() && NodeBoardKind(m) == "" {
|
||||
return d2parser.Errorf(kp.Path[i].Unbox(), "%s is only allowed at a board root", headString)
|
||||
}
|
||||
|
||||
for _, f := range m.Fields {
|
||||
if !strings.EqualFold(f.Name, head) {
|
||||
if !(f.Name != nil && strings.EqualFold(f.Name.ScalarString(), head.ScalarString()) && f.Name.IsUnquoted() == head.IsUnquoted()) {
|
||||
continue
|
||||
}
|
||||
|
||||
|
|
@ -872,14 +932,14 @@ func (m *Map) ensureField(i int, kp *d2ast.KeyPath, refctx *RefContext, create b
|
|||
return nil
|
||||
}
|
||||
shape := ParentShape(m)
|
||||
if _, ok := d2graph.ReservedKeywords[strings.ToLower(head)]; !ok && len(c.globRefContextStack) > 0 {
|
||||
if _, ok := d2ast.ReservedKeywords[strings.ToLower(head.ScalarString())]; !(ok && head.IsUnquoted()) && len(c.globRefContextStack) > 0 {
|
||||
if shape == d2target.ShapeClass || shape == d2target.ShapeSQLTable {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
f := &Field{
|
||||
parent: m,
|
||||
Name: head,
|
||||
Name: kp.Path[i].Unbox(),
|
||||
}
|
||||
defer func() {
|
||||
if i < kp.FirstGlob() {
|
||||
|
|
@ -887,10 +947,10 @@ func (m *Map) ensureField(i int, kp *d2ast.KeyPath, refctx *RefContext, create b
|
|||
}
|
||||
for _, grefctx := range c.globRefContextStack {
|
||||
var ks string
|
||||
if grefctx.Key.HasTripleGlob() {
|
||||
ks = d2format.Format(d2ast.MakeKeyPath(IDA(f)))
|
||||
if grefctx.Key.HasMultiGlob() {
|
||||
ks = d2format.Format(d2ast.MakeKeyPathString(IDA(f)))
|
||||
} else {
|
||||
ks = d2format.Format(d2ast.MakeKeyPath(BoardIDA(f)))
|
||||
ks = d2format.Format(d2ast.MakeKeyPathString(BoardIDA(f)))
|
||||
}
|
||||
gctx2 := c.getGlobContext(grefctx)
|
||||
gctx2.appliedFields[ks] = struct{}{}
|
||||
|
|
@ -927,9 +987,25 @@ func (m *Map) DeleteEdge(eid *EdgeID) *Edge {
|
|||
return nil
|
||||
}
|
||||
|
||||
for i, e := range m.Edges {
|
||||
if e.ID.Match(eid) {
|
||||
m.Edges = append(m.Edges[:i], m.Edges[i+1:]...)
|
||||
resolvedEID, resolvedM, common, err := eid.resolve(m)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(common) > 0 {
|
||||
f := resolvedM.GetField(common...)
|
||||
if f == nil {
|
||||
return nil
|
||||
}
|
||||
if f.Map() == nil {
|
||||
return nil
|
||||
}
|
||||
return f.Map().DeleteEdge(resolvedEID)
|
||||
}
|
||||
|
||||
for i, e := range resolvedM.Edges {
|
||||
if e.ID.Match(resolvedEID) {
|
||||
resolvedM.Edges = append(resolvedM.Edges[:i], resolvedM.Edges[i+1:]...)
|
||||
return e
|
||||
}
|
||||
}
|
||||
|
|
@ -945,7 +1021,7 @@ func (m *Map) DeleteField(ida ...string) *Field {
|
|||
rest := ida[1:]
|
||||
|
||||
for i, f := range m.Fields {
|
||||
if !strings.EqualFold(f.Name, s) {
|
||||
if !strings.EqualFold(f.Name.ScalarString(), s) {
|
||||
continue
|
||||
}
|
||||
if len(rest) == 0 {
|
||||
|
|
@ -971,11 +1047,11 @@ func (m *Map) DeleteField(ida ...string) *Field {
|
|||
// If a field was deleted from a keyword-holder keyword and that holder is empty,
|
||||
// then that holder becomes meaningless and should be deleted too
|
||||
parent := ParentField(f)
|
||||
for keywordHolder := range d2graph.ReservedKeywordHolders {
|
||||
if parent != nil && parent.Name == keywordHolder && len(parent.Map().Fields) == 0 {
|
||||
for keywordHolder := range d2ast.ReservedKeywordHolders {
|
||||
if parent != nil && parent.Name.ScalarString() == keywordHolder && parent.Name.IsUnquoted() && len(parent.Map().Fields) == 0 {
|
||||
keywordHolderParentMap := ParentMap(parent)
|
||||
for i, f := range keywordHolderParentMap.Fields {
|
||||
if f.Name == keywordHolder {
|
||||
if f.Name.ScalarString() == keywordHolder && f.Name.IsUnquoted() {
|
||||
keywordHolderParentMap.Fields = append(keywordHolderParentMap.Fields[:i], keywordHolderParentMap.Fields[i+1:]...)
|
||||
break
|
||||
}
|
||||
|
|
@ -1033,7 +1109,7 @@ func (m *Map) getEdges(eid *EdgeID, refctx *RefContext, gctx *globContext, ea *[
|
|||
}
|
||||
|
||||
if len(common) > 0 {
|
||||
commonKP := d2ast.MakeKeyPath(common)
|
||||
commonKP := d2ast.MakeKeyPathString(common)
|
||||
lastMatch := 0
|
||||
for i, el := range commonKP.Path {
|
||||
for j := lastMatch; j < len(refctx.Edge.Src.Path); j++ {
|
||||
|
|
@ -1084,10 +1160,10 @@ func (m *Map) getEdges(eid *EdgeID, refctx *RefContext, gctx *globContext, ea *[
|
|||
for _, e := range ea2 {
|
||||
if gctx != nil {
|
||||
var ks string
|
||||
if refctx.Key.HasTripleGlob() {
|
||||
ks = d2format.Format(d2ast.MakeKeyPath(IDA(e)))
|
||||
if refctx.Key.HasMultiGlob() {
|
||||
ks = d2format.Format(d2ast.MakeKeyPathString(IDA(e)))
|
||||
} else {
|
||||
ks = d2format.Format(d2ast.MakeKeyPath(BoardIDA(e)))
|
||||
ks = d2format.Format(d2ast.MakeKeyPathString(BoardIDA(e)))
|
||||
}
|
||||
if _, ok := gctx.appliedEdges[ks]; ok {
|
||||
continue
|
||||
|
|
@ -1129,7 +1205,7 @@ func (m *Map) createEdge(eid *EdgeID, refctx *RefContext, gctx *globContext, c *
|
|||
return d2parser.Errorf(refctx.Edge, err.Error())
|
||||
}
|
||||
if len(common) > 0 {
|
||||
commonKP := d2ast.MakeKeyPath(common)
|
||||
commonKP := d2ast.MakeKeyPathString(common)
|
||||
lastMatch := 0
|
||||
for i, el := range commonKP.Path {
|
||||
for j := lastMatch; j < len(refctx.Edge.Src.Path); j++ {
|
||||
|
|
@ -1218,11 +1294,11 @@ func (m *Map) createEdge(eid *EdgeID, refctx *RefContext, gctx *globContext, c *
|
|||
eid2.SrcPath = RelIDA(m, src)
|
||||
eid2.DstPath = RelIDA(m, dst)
|
||||
|
||||
e, err := m.createEdge2(eid2, refctx, gctx, c, src, dst)
|
||||
es, err := m.createEdge2(eid2, refctx, gctx, c, src, dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if e != nil {
|
||||
for _, e := range es {
|
||||
*ea = append(*ea, e)
|
||||
}
|
||||
}
|
||||
|
|
@ -1230,7 +1306,7 @@ func (m *Map) createEdge(eid *EdgeID, refctx *RefContext, gctx *globContext, c *
|
|||
return nil
|
||||
}
|
||||
|
||||
func (m *Map) createEdge2(eid *EdgeID, refctx *RefContext, gctx *globContext, c *compiler, src, dst *Field) (*Edge, error) {
|
||||
func (m *Map) createEdge2(eid *EdgeID, refctx *RefContext, gctx *globContext, c *compiler, src, dst *Field) ([]*Edge, error) {
|
||||
if NodeBoardKind(src) != "" {
|
||||
return nil, d2parser.Errorf(refctx.Edge.Src, "cannot create edges between boards")
|
||||
}
|
||||
|
|
@ -1241,6 +1317,45 @@ func (m *Map) createEdge2(eid *EdgeID, refctx *RefContext, gctx *globContext, c
|
|||
return nil, d2parser.Errorf(refctx.Edge, "cannot create edges between boards")
|
||||
}
|
||||
|
||||
eid, m, common, err := eid.resolve(m)
|
||||
if err != nil {
|
||||
return nil, d2parser.Errorf(refctx.Edge, err.Error())
|
||||
}
|
||||
if len(common) > 0 {
|
||||
commonKP := d2ast.MakeKeyPathString(common)
|
||||
lastMatch := 0
|
||||
for i, el := range commonKP.Path {
|
||||
for j := lastMatch; j < len(refctx.Edge.Src.Path); j++ {
|
||||
realEl := refctx.Edge.Src.Path[j]
|
||||
if el.ScalarString() == realEl.ScalarString() {
|
||||
commonKP.Path[i] = realEl
|
||||
lastMatch += j + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
fa, err := m.EnsureField(commonKP, nil, true, c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var edges []*Edge
|
||||
for _, f := range fa {
|
||||
if _, ok := f.Composite.(*Array); ok {
|
||||
return nil, d2parser.Errorf(refctx.Edge.Src, "cannot index into array")
|
||||
}
|
||||
if f.Map() == nil {
|
||||
f.Composite = &Map{
|
||||
parent: f,
|
||||
}
|
||||
}
|
||||
edges2, err := f.Map().createEdge2(eid, refctx, gctx, c, src, dst)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
edges = append(edges, edges2...)
|
||||
}
|
||||
return edges, nil
|
||||
}
|
||||
|
||||
eid.Index = nil
|
||||
eid.Glob = true
|
||||
ea := m.GetEdges(eid, nil, nil)
|
||||
|
|
@ -1263,10 +1378,10 @@ func (m *Map) createEdge2(eid *EdgeID, refctx *RefContext, gctx *globContext, c
|
|||
e2 := e.Copy(e.Parent()).(*Edge)
|
||||
e2.ID = e2.ID.Copy()
|
||||
e2.ID.Index = nil
|
||||
if refctx.Key.HasTripleGlob() {
|
||||
ks = d2format.Format(d2ast.MakeKeyPath(IDA(e2)))
|
||||
if refctx.Key.HasMultiGlob() {
|
||||
ks = d2format.Format(d2ast.MakeKeyPathString(IDA(e2)))
|
||||
} else {
|
||||
ks = d2format.Format(d2ast.MakeKeyPath(BoardIDA(e2)))
|
||||
ks = d2format.Format(d2ast.MakeKeyPathString(BoardIDA(e2)))
|
||||
}
|
||||
if _, ok := gctx.appliedEdges[ks]; ok {
|
||||
return nil, nil
|
||||
|
|
@ -1276,7 +1391,7 @@ func (m *Map) createEdge2(eid *EdgeID, refctx *RefContext, gctx *globContext, c
|
|||
|
||||
m.Edges = append(m.Edges, e)
|
||||
|
||||
return e, nil
|
||||
return []*Edge{e}, nil
|
||||
}
|
||||
|
||||
func (s *Scalar) AST() d2ast.Node {
|
||||
|
|
@ -1287,7 +1402,7 @@ func (f *Field) AST() d2ast.Node {
|
|||
k := &d2ast.Key{
|
||||
Key: &d2ast.KeyPath{
|
||||
Path: []*d2ast.StringBox{
|
||||
d2ast.MakeValueBox(d2ast.RawString(f.Name, true)).StringBox(),
|
||||
d2ast.MakeValueBox(f.Name).StringBox(),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
@ -1296,7 +1411,14 @@ func (f *Field) AST() d2ast.Node {
|
|||
k.Primary = d2ast.MakeValueBox(f.Primary_.AST().(d2ast.Value)).ScalarBox()
|
||||
}
|
||||
if f.Composite != nil {
|
||||
k.Value = d2ast.MakeValueBox(f.Composite.AST().(d2ast.Value))
|
||||
value := f.Composite.AST().(d2ast.Value)
|
||||
if m, ok := value.(*d2ast.Map); ok {
|
||||
path := m.Range.Path
|
||||
// Treat it as multi-line, but not file-map (line 0)
|
||||
m.Range = d2ast.MakeRange(",1:0:0-2:0:0")
|
||||
m.Range.Path = path
|
||||
}
|
||||
k.Value = d2ast.MakeValueBox(value)
|
||||
}
|
||||
|
||||
return k
|
||||
|
|
@ -1305,11 +1427,11 @@ func (f *Field) AST() d2ast.Node {
|
|||
func (e *Edge) AST() d2ast.Node {
|
||||
astEdge := &d2ast.Edge{}
|
||||
|
||||
astEdge.Src = d2ast.MakeKeyPath(e.ID.SrcPath)
|
||||
astEdge.Src = d2ast.MakeKeyPathString(e.ID.SrcPath)
|
||||
if e.ID.SrcArrow {
|
||||
astEdge.SrcArrow = "<"
|
||||
}
|
||||
astEdge.Dst = d2ast.MakeKeyPath(e.ID.DstPath)
|
||||
astEdge.Dst = d2ast.MakeKeyPathString(e.ID.DstPath)
|
||||
if e.ID.DstArrow {
|
||||
astEdge.DstArrow = ">"
|
||||
}
|
||||
|
|
@ -1328,7 +1450,7 @@ func (e *Edge) AST() d2ast.Node {
|
|||
return k
|
||||
}
|
||||
|
||||
func (e *Edge) IDString() string {
|
||||
func (e *Edge) IDString() d2ast.String {
|
||||
ast := e.AST().(*d2ast.Key)
|
||||
if e.ID.Index != nil {
|
||||
ast.EdgeIndex = &d2ast.EdgeIndex{
|
||||
|
|
@ -1337,7 +1459,8 @@ func (e *Edge) IDString() string {
|
|||
}
|
||||
ast.Primary = d2ast.ScalarBox{}
|
||||
ast.Value = d2ast.ValueBox{}
|
||||
return d2format.Format(ast)
|
||||
formatted := d2format.Format(ast)
|
||||
return d2ast.FlatUnquotedString(formatted)
|
||||
}
|
||||
|
||||
func (a *Array) AST() d2ast.Node {
|
||||
|
|
@ -1355,11 +1478,14 @@ func (m *Map) AST() d2ast.Node {
|
|||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
astMap := &d2ast.Map{}
|
||||
if m.Root() {
|
||||
astMap.Range = d2ast.MakeRange(",0:0:0-1:0:0")
|
||||
} else {
|
||||
astMap.Range = d2ast.MakeRange(",1:0:0-2:0:0")
|
||||
astMap := &d2ast.Map{
|
||||
Range: d2ast.MakeRange(",0:0:0-1:0:0"),
|
||||
}
|
||||
if m.parent != nil && NodeBoardKind(m) != "" {
|
||||
f, ok := m.parent.(*Field)
|
||||
if ok {
|
||||
astMap.Range.Path = f.Name.GetRange().Path
|
||||
}
|
||||
}
|
||||
for _, f := range m.Fields {
|
||||
astMap.Nodes = append(astMap.Nodes, d2ast.MakeMapNodeBox(f.AST().(d2ast.MapNode)))
|
||||
|
|
@ -1372,7 +1498,7 @@ func (m *Map) AST() d2ast.Node {
|
|||
|
||||
func (m *Map) appendFieldReferences(i int, kp *d2ast.KeyPath, refctx *RefContext, c *compiler) {
|
||||
sb := kp.Path[i]
|
||||
f := m.GetField(sb.Unbox().ScalarString())
|
||||
f := m.GetField(sb.Unbox())
|
||||
if f == nil {
|
||||
return
|
||||
}
|
||||
|
|
@ -1431,9 +1557,12 @@ func IsVar(n Node) bool {
|
|||
if NodeBoardKind(n) != "" {
|
||||
return false
|
||||
}
|
||||
if f, ok := n.(*Field); ok && f.Name == "vars" {
|
||||
if f, ok := n.(*Field); ok && f.Name.ScalarString() == "vars" && f.Name.IsUnquoted() {
|
||||
return true
|
||||
}
|
||||
if n == (*Map)(nil) {
|
||||
return false
|
||||
}
|
||||
n = n.Parent()
|
||||
}
|
||||
}
|
||||
|
|
@ -1467,7 +1596,7 @@ func ParentShape(n Node) string {
|
|||
f, ok := n.(*Field)
|
||||
if ok {
|
||||
if f.Map() != nil {
|
||||
shapef := f.Map().GetField("shape")
|
||||
shapef := f.Map().GetField(d2ast.FlatUnquotedString("shape"))
|
||||
if shapef != nil && shapef.Primary() != nil {
|
||||
return shapef.Primary().Value.ScalarString()
|
||||
}
|
||||
|
|
@ -1480,30 +1609,30 @@ func ParentShape(n Node) string {
|
|||
}
|
||||
}
|
||||
|
||||
func countUnderscores(p []string) int {
|
||||
func countUnderscores(p []d2ast.String) int {
|
||||
for i, el := range p {
|
||||
if el != "_" {
|
||||
if el.ScalarString() != "_" || !el.IsUnquoted() {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func findBoardKeyword(ida ...string) int {
|
||||
func findBoardKeyword(ida ...d2ast.String) int {
|
||||
for i := range ida {
|
||||
if _, ok := d2graph.BoardKeywords[ida[i]]; ok {
|
||||
if _, ok := d2ast.BoardKeywords[strings.ToLower(ida[i].ScalarString())]; ok && ida[i].IsUnquoted() {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func findProhibitedEdgeKeyword(ida ...string) int {
|
||||
func findProhibitedEdgeKeyword(ida ...d2ast.String) int {
|
||||
for i := range ida {
|
||||
if _, ok := d2graph.SimpleReservedKeywords[ida[i]]; ok {
|
||||
if _, ok := d2ast.SimpleReservedKeywords[ida[i].ScalarString()]; ok && ida[i].IsUnquoted() {
|
||||
return i
|
||||
}
|
||||
if _, ok := d2graph.ReservedKeywordHolders[ida[i]]; ok {
|
||||
if _, ok := d2ast.ReservedKeywordHolders[ida[i].ScalarString()]; ok && ida[i].IsUnquoted() {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
|
@ -1547,7 +1676,7 @@ func parentPrimaryKey(n Node) *d2ast.Key {
|
|||
}
|
||||
|
||||
// BoardIDA returns the absolute path to n from the nearest board root.
|
||||
func BoardIDA(n Node) (ida []string) {
|
||||
func BoardIDA(n Node) (ida []d2ast.String) {
|
||||
for {
|
||||
switch n := n.(type) {
|
||||
case *Field:
|
||||
|
|
@ -1568,7 +1697,7 @@ func BoardIDA(n Node) (ida []string) {
|
|||
}
|
||||
|
||||
// IDA returns the absolute path to n.
|
||||
func IDA(n Node) (ida []string) {
|
||||
func IDA(n Node) (ida []d2ast.String) {
|
||||
for {
|
||||
switch n := n.(type) {
|
||||
case *Field:
|
||||
|
|
@ -1589,7 +1718,7 @@ func IDA(n Node) (ida []string) {
|
|||
}
|
||||
|
||||
// RelIDA returns the path to n relative to p.
|
||||
func RelIDA(p, n Node) (ida []string) {
|
||||
func RelIDA(p, n Node) (ida []d2ast.String) {
|
||||
for {
|
||||
switch n := n.(type) {
|
||||
case *Field:
|
||||
|
|
@ -1599,7 +1728,7 @@ func RelIDA(p, n Node) (ida []string) {
|
|||
return ida
|
||||
}
|
||||
case *Edge:
|
||||
ida = append(ida, n.String())
|
||||
ida = append(ida, d2ast.FlatUnquotedString(n.String()))
|
||||
}
|
||||
n = n.Parent()
|
||||
f, fok := n.(*Field)
|
||||
|
|
@ -1611,11 +1740,11 @@ func RelIDA(p, n Node) (ida []string) {
|
|||
}
|
||||
}
|
||||
|
||||
func reverseIDA(ida []string) {
|
||||
for i := 0; i < len(ida)/2; i++ {
|
||||
tmp := ida[i]
|
||||
ida[i] = ida[len(ida)-i-1]
|
||||
ida[len(ida)-i-1] = tmp
|
||||
func reverseIDA[T any](slice []T) {
|
||||
for i := 0; i < len(slice)/2; i++ {
|
||||
tmp := slice[i]
|
||||
slice[i] = slice[len(slice)-i-1]
|
||||
slice[len(slice)-i-1] = tmp
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1690,7 +1819,7 @@ func (m *Map) Equal(n2 Node) bool {
|
|||
}
|
||||
|
||||
func (m *Map) InClass(key *d2ast.Key) bool {
|
||||
classes := m.Map().GetField("classes")
|
||||
classes := m.Map().GetField(d2ast.FlatUnquotedString("classes"))
|
||||
if classes == nil || classes.Map() == nil {
|
||||
return false
|
||||
}
|
||||
|
|
@ -1718,7 +1847,7 @@ func (m *Map) IsClass() bool {
|
|||
if parentBoard.Map() == nil {
|
||||
return false
|
||||
}
|
||||
classes := parentBoard.Map().GetField("classes")
|
||||
classes := parentBoard.Map().GetField(d2ast.FlatUnquotedString("classes"))
|
||||
if classes == nil || classes.Map() == nil {
|
||||
return false
|
||||
}
|
||||
|
|
@ -1730,3 +1859,51 @@ func (m *Map) IsClass() bool {
|
|||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *Map) FindBoardRoot(path []string) *Map {
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
if len(path) == 0 {
|
||||
return m
|
||||
}
|
||||
|
||||
layersf := m.GetField(d2ast.FlatUnquotedString("layers"))
|
||||
scenariosf := m.GetField(d2ast.FlatUnquotedString("scenarios"))
|
||||
stepsf := m.GetField(d2ast.FlatUnquotedString("steps"))
|
||||
|
||||
if layersf != nil && layersf.Map() != nil {
|
||||
for _, f := range layersf.Map().Fields {
|
||||
if f.Name.ScalarString() == path[0] {
|
||||
if len(path) == 1 {
|
||||
return f.Map()
|
||||
}
|
||||
return f.Map().FindBoardRoot(path[1:])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if scenariosf != nil && scenariosf.Map() != nil {
|
||||
for _, f := range scenariosf.Map().Fields {
|
||||
if f.Name.ScalarString() == path[0] {
|
||||
if len(path) == 1 {
|
||||
return f.Map()
|
||||
}
|
||||
return f.Map().FindBoardRoot(path[1:])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if stepsf != nil && stepsf.Map() != nil {
|
||||
for _, f := range stepsf.Map().Fields {
|
||||
if f.Name.ScalarString() == path[0] {
|
||||
if len(path) == 1 {
|
||||
return f.Map()
|
||||
}
|
||||
return f.Map().FindBoardRoot(path[1:])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ func TestCopy(t *testing.T) {
|
|||
|
||||
const keyStr = `Absence makes the heart grow frantic.`
|
||||
f := &d2ir.Field{
|
||||
Name: keyStr,
|
||||
Name: d2ast.FlatUnquotedString(keyStr),
|
||||
|
||||
Primary_: s,
|
||||
Composite: a,
|
||||
|
|
@ -48,10 +48,10 @@ func TestCopy(t *testing.T) {
|
|||
}
|
||||
|
||||
m = m.Copy(nil).(*d2ir.Map)
|
||||
f.Name = `Many a wife thinks her husband is the world's greatest lover.`
|
||||
f.Name = d2ast.FlatUnquotedString(`Many a wife thinks her husband is the world's greatest lover.`)
|
||||
|
||||
assert.Equal(t, m, m.Fields[0].Parent())
|
||||
assert.Equal(t, keyStr, m.Fields[0].Name)
|
||||
assert.Equal(t, keyStr, m.Fields[0].Name.ScalarString())
|
||||
assert.Equal(t, m.Fields[0], m.Fields[0].Primary_.Parent())
|
||||
assert.Equal(t, m.Fields[0], m.Fields[0].Composite.(*d2ir.Array).Parent())
|
||||
|
||||
|
|
|
|||
|
|
@ -175,7 +175,6 @@ x -> y: hi
|
|||
}
|
||||
|
||||
a
|
||||
# if i remove this line, the glob applies as expected
|
||||
b
|
||||
b.label: a
|
||||
`)
|
||||
|
|
@ -225,6 +224,25 @@ classes: {
|
|||
assertQuery(t, m, 9, 3, nil, "")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "not-basic",
|
||||
run: func(t testing.TB) {
|
||||
m, err := compile(t, `jacob: {
|
||||
shape: circle
|
||||
}
|
||||
jeremy: {
|
||||
shape: rectangle
|
||||
}
|
||||
*: {
|
||||
!&shape: rectangle
|
||||
label: I'm not a rectangle
|
||||
}`)
|
||||
assert.Success(t, err)
|
||||
assertQuery(t, m, 2, 0, nil, "jacob")
|
||||
assertQuery(t, m, 1, 0, nil, "jeremy")
|
||||
assertQuery(t, m, 0, 0, "I'm not a rectangle", "jacob.label")
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
runa(t, tca)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"oss.terrastruct.com/d2/d2ast"
|
||||
|
|
@ -17,17 +18,13 @@ func (c *compiler) pushImportStack(imp *d2ast.Import) (string, bool) {
|
|||
return "", false
|
||||
}
|
||||
if len(c.importStack) > 0 {
|
||||
if path.IsAbs(impPath) {
|
||||
c.errorf(imp, "import paths must be relative")
|
||||
return "", false
|
||||
}
|
||||
|
||||
if path.Ext(impPath) != ".d2" {
|
||||
impPath += ".d2"
|
||||
}
|
||||
|
||||
// Imports are always relative to the importing file.
|
||||
impPath = path.Join(path.Dir(c.importStack[len(c.importStack)-1]), impPath)
|
||||
if !filepath.IsAbs(impPath) {
|
||||
impPath = path.Join(path.Dir(c.importStack[len(c.importStack)-1]), impPath)
|
||||
}
|
||||
}
|
||||
|
||||
for i, p := range c.importStack {
|
||||
|
|
|
|||
|
|
@ -211,6 +211,23 @@ label: meow`,
|
|||
assert.Success(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nested-scope",
|
||||
run: func(t testing.TB) {
|
||||
m, err := compileFS(t, "index.d2", map[string]string{
|
||||
"index.d2": `...@second
|
||||
`,
|
||||
"second.d2": `elem: {
|
||||
...@third
|
||||
}`,
|
||||
"third.d2": `third: {
|
||||
elem
|
||||
}`,
|
||||
})
|
||||
assert.Success(t, err)
|
||||
assertQuery(t, m, 3, 0, nil, "")
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
runa(t, tca)
|
||||
|
|
@ -235,15 +252,6 @@ label: meow`,
|
|||
assert.ErrorString(t, err, `index.d2:1:1: failed to import "../x.d2": open ../x.d2: invalid argument`)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "absolute",
|
||||
run: func(t testing.TB) {
|
||||
_, err := compileFS(t, "index.d2", map[string]string{
|
||||
"index.d2": "...@/x.d2",
|
||||
})
|
||||
assert.ErrorString(t, err, `index.d2:1:1: import paths must be relative`)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "parse",
|
||||
run: func(t testing.TB) {
|
||||
|
|
|
|||
|
|
@ -21,6 +21,39 @@ func OverlayMap(base, overlay *Map) {
|
|||
}
|
||||
}
|
||||
|
||||
func ExpandSubstitution(m, resolved *Map, placeholder *Field) {
|
||||
fi := -1
|
||||
for i := 0; i < len(m.Fields); i++ {
|
||||
if m.Fields[i] == placeholder {
|
||||
fi = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for _, of := range resolved.Fields {
|
||||
bf := m.GetField(of.Name)
|
||||
if bf == nil {
|
||||
m.Fields = append(m.Fields[:fi], append([]*Field{of.Copy(m).(*Field)}, m.Fields[fi:]...)...)
|
||||
fi++
|
||||
continue
|
||||
}
|
||||
OverlayField(bf, of)
|
||||
}
|
||||
|
||||
// NOTE this doesn't expand edges in place, and just appends
|
||||
// I suppose to do this, there needs to be an edge placeholder too on top of the field placeholder
|
||||
// Will wait to see if a problem
|
||||
for _, oe := range resolved.Edges {
|
||||
bea := m.GetEdges(oe.ID, nil, nil)
|
||||
if len(bea) == 0 {
|
||||
m.Edges = append(m.Edges, oe.Copy(m).(*Edge))
|
||||
continue
|
||||
}
|
||||
be := bea[0]
|
||||
OverlayEdge(be, oe)
|
||||
}
|
||||
}
|
||||
|
||||
func OverlayField(bf, of *Field) {
|
||||
if of.Primary_ != nil {
|
||||
bf.Primary_ = of.Primary_.Copy(bf).(*Scalar)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import (
|
|||
"strings"
|
||||
|
||||
"oss.terrastruct.com/d2/d2ast"
|
||||
"oss.terrastruct.com/d2/d2graph"
|
||||
)
|
||||
|
||||
func (m *Map) multiGlob(pattern []string) ([]*Field, bool) {
|
||||
|
|
@ -22,11 +21,14 @@ func (m *Map) multiGlob(pattern []string) ([]*Field, bool) {
|
|||
|
||||
func (m *Map) _doubleGlob(fa *[]*Field) {
|
||||
for _, f := range m.Fields {
|
||||
if _, ok := d2graph.ReservedKeywords[f.Name]; ok {
|
||||
if f.Name == "layers" {
|
||||
if f.Name == nil {
|
||||
continue
|
||||
}
|
||||
if _, ok := d2ast.ReservedKeywords[f.Name.ScalarString()]; ok && f.Name.IsUnquoted() {
|
||||
if f.Name.ScalarString() == "layers" {
|
||||
continue
|
||||
}
|
||||
if _, ok := d2graph.BoardKeywords[f.Name]; !ok {
|
||||
if _, ok := d2ast.BoardKeywords[f.Name.ScalarString()]; !ok {
|
||||
continue
|
||||
}
|
||||
// We don't ever want to append layers, scenarios or steps directly.
|
||||
|
|
@ -46,8 +48,8 @@ func (m *Map) _doubleGlob(fa *[]*Field) {
|
|||
|
||||
func (m *Map) _tripleGlob(fa *[]*Field) {
|
||||
for _, f := range m.Fields {
|
||||
if _, ok := d2graph.ReservedKeywords[f.Name]; ok {
|
||||
if _, ok := d2graph.BoardKeywords[f.Name]; !ok {
|
||||
if _, ok := d2ast.ReservedKeywords[f.Name.ScalarString()]; ok && f.Name.IsUnquoted() {
|
||||
if _, ok := d2ast.BoardKeywords[f.Name.ScalarString()]; !ok {
|
||||
continue
|
||||
}
|
||||
// We don't ever want to append layers, scenarios or steps directly.
|
||||
|
|
@ -69,7 +71,7 @@ func matchPattern(s string, pattern []string) bool {
|
|||
if len(pattern) == 0 {
|
||||
return true
|
||||
}
|
||||
if _, ok := d2graph.ReservedKeywords[s]; ok {
|
||||
if _, ok := d2ast.ReservedKeywords[s]; ok {
|
||||
return false
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -157,6 +157,24 @@ sh*.an* -> sh*.an*`)
|
|||
assertQuery(t, m, 0, 0, nil, "shared.(animal -> animate)[0]")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "edge/4",
|
||||
run: func(t testing.TB) {
|
||||
m, err := compile(t, `app_a: {
|
||||
x
|
||||
}
|
||||
|
||||
app_b: {
|
||||
y
|
||||
}
|
||||
|
||||
app_*.x -> app_*.y`)
|
||||
assert.Success(t, err)
|
||||
assertQuery(t, m, 6, 4, nil, "")
|
||||
assertQuery(t, m, 2, 1, nil, "app_a")
|
||||
assertQuery(t, m, 2, 1, nil, "app_b")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "edge-glob-index",
|
||||
run: func(t testing.TB) {
|
||||
|
|
@ -310,6 +328,79 @@ layers.x: { wrapper.p }
|
|||
assertQuery(t, m, 0, 0, nil, "layers.x.wrapper.p")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "edge-glob-null",
|
||||
run: func(t testing.TB) {
|
||||
m, err := compile(t, `a -> b
|
||||
(* -> *)[*]: null
|
||||
x -> y
|
||||
`)
|
||||
assert.Success(t, err)
|
||||
// 4 fields and 0 edges
|
||||
assertQuery(t, m, 4, 0, nil, "")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "field-glob-style-inherit",
|
||||
run: func(t testing.TB) {
|
||||
m, err := compile(t, `*.style.opacity: 0
|
||||
x: {
|
||||
style.opacity: 1
|
||||
}
|
||||
|
||||
scenarios: {
|
||||
1: {
|
||||
x
|
||||
}
|
||||
}
|
||||
`)
|
||||
assert.Success(t, err)
|
||||
assertQuery(t, m, 0, 0, 1, "x.style.opacity")
|
||||
assertQuery(t, m, 0, 0, 1, "scenarios.1.x.style.opacity")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "edge-glob-style-inherit/1",
|
||||
run: func(t testing.TB) {
|
||||
m, err := compile(t, `(* -> *)[*].style.opacity: 0
|
||||
x -> y: {
|
||||
style.opacity: 1
|
||||
}
|
||||
|
||||
scenarios: {
|
||||
1: {
|
||||
x
|
||||
}
|
||||
}
|
||||
`)
|
||||
assert.Success(t, err)
|
||||
assertQuery(t, m, 0, 0, 1, "(x -> y)[0].style.opacity")
|
||||
assertQuery(t, m, 0, 0, 1, "scenarios.1.(x -> y)[0].style.opacity")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "edge-glob-style-inherit/2",
|
||||
run: func(t testing.TB) {
|
||||
m, err := compile(t, `*.style.opacity: 0
|
||||
(* -> *)[*].style.opacity: 0
|
||||
x -> y
|
||||
|
||||
steps: {
|
||||
1: {
|
||||
x.style.opacity: 1
|
||||
}
|
||||
2: {
|
||||
(x -> y)[0].style.opacity: 1
|
||||
}
|
||||
3: {
|
||||
y.style.opacity: 1
|
||||
}
|
||||
}
|
||||
`)
|
||||
assert.Success(t, err)
|
||||
assertQuery(t, m, 0, 0, 1, "steps.3.(x -> y)[0].style.opacity")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "double-glob/edge/1",
|
||||
run: func(t testing.TB) {
|
||||
|
|
|
|||
71
d2js/d2wasm/api.go
Normal file
71
d2js/d2wasm/api.go
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
//go:build js && wasm
|
||||
|
||||
package d2wasm
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"runtime/debug"
|
||||
"syscall/js"
|
||||
)
|
||||
|
||||
type D2API struct {
|
||||
exports map[string]js.Func
|
||||
}
|
||||
|
||||
func NewD2API() *D2API {
|
||||
return &D2API{
|
||||
exports: make(map[string]js.Func),
|
||||
}
|
||||
}
|
||||
|
||||
func (api *D2API) Register(name string, fn func(args []js.Value) (interface{}, error)) {
|
||||
api.exports[name] = wrapWASMCall(fn)
|
||||
}
|
||||
|
||||
func (api *D2API) ExportTo(target js.Value) {
|
||||
d2Namespace := make(map[string]interface{})
|
||||
for name, fn := range api.exports {
|
||||
d2Namespace[name] = fn
|
||||
}
|
||||
target.Set("d2", js.ValueOf(d2Namespace))
|
||||
}
|
||||
|
||||
func wrapWASMCall(fn func(args []js.Value) (interface{}, error)) js.Func {
|
||||
return js.FuncOf(func(this js.Value, args []js.Value) (result any) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
resp := WASMResponse{
|
||||
Error: &WASMError{
|
||||
Message: fmt.Sprintf("panic recovered: %v\n%s", r, debug.Stack()),
|
||||
Code: 500,
|
||||
},
|
||||
}
|
||||
jsonResp, _ := json.Marshal(resp)
|
||||
result = string(jsonResp)
|
||||
}
|
||||
}()
|
||||
|
||||
data, err := fn(args)
|
||||
if err != nil {
|
||||
wasmErr, ok := err.(*WASMError)
|
||||
if !ok {
|
||||
wasmErr = &WASMError{
|
||||
Message: err.Error(),
|
||||
Code: 500,
|
||||
}
|
||||
}
|
||||
resp := WASMResponse{
|
||||
Error: wasmErr,
|
||||
}
|
||||
jsonResp, _ := json.Marshal(resp)
|
||||
return string(jsonResp)
|
||||
}
|
||||
|
||||
resp := WASMResponse{
|
||||
Data: data,
|
||||
}
|
||||
jsonResp, _ := json.Marshal(resp)
|
||||
return string(jsonResp)
|
||||
})
|
||||
}
|
||||
338
d2js/d2wasm/functions.go
Normal file
338
d2js/d2wasm/functions.go
Normal file
|
|
@ -0,0 +1,338 @@
|
|||
//go:build js && wasm
|
||||
|
||||
package d2wasm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"syscall/js"
|
||||
|
||||
"oss.terrastruct.com/d2/d2ast"
|
||||
"oss.terrastruct.com/d2/d2compiler"
|
||||
"oss.terrastruct.com/d2/d2format"
|
||||
"oss.terrastruct.com/d2/d2graph"
|
||||
"oss.terrastruct.com/d2/d2layouts/d2dagrelayout"
|
||||
"oss.terrastruct.com/d2/d2layouts/d2elklayout"
|
||||
"oss.terrastruct.com/d2/d2lib"
|
||||
"oss.terrastruct.com/d2/d2lsp"
|
||||
"oss.terrastruct.com/d2/d2oracle"
|
||||
"oss.terrastruct.com/d2/d2parser"
|
||||
"oss.terrastruct.com/d2/d2renderers/d2fonts"
|
||||
"oss.terrastruct.com/d2/d2renderers/d2svg"
|
||||
"oss.terrastruct.com/d2/lib/log"
|
||||
"oss.terrastruct.com/d2/lib/memfs"
|
||||
"oss.terrastruct.com/d2/lib/textmeasure"
|
||||
"oss.terrastruct.com/d2/lib/urlenc"
|
||||
"oss.terrastruct.com/d2/lib/version"
|
||||
"oss.terrastruct.com/util-go/go2"
|
||||
)
|
||||
|
||||
func GetParentID(args []js.Value) (interface{}, error) {
|
||||
if len(args) < 1 {
|
||||
return nil, &WASMError{Message: "missing id argument", Code: 400}
|
||||
}
|
||||
|
||||
id := args[0].String()
|
||||
mk, err := d2parser.ParseMapKey(id)
|
||||
if err != nil {
|
||||
return nil, &WASMError{Message: err.Error(), Code: 400}
|
||||
}
|
||||
|
||||
if len(mk.Edges) > 0 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if mk.Key != nil {
|
||||
if len(mk.Key.Path) == 1 {
|
||||
return "root", nil
|
||||
}
|
||||
mk.Key.Path = mk.Key.Path[:len(mk.Key.Path)-1]
|
||||
return strings.Join(mk.Key.StringIDA(), "."), nil
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func GetObjOrder(args []js.Value) (interface{}, error) {
|
||||
if len(args) < 1 {
|
||||
return nil, &WASMError{Message: "missing dsl argument", Code: 400}
|
||||
}
|
||||
|
||||
dsl := args[0].String()
|
||||
g, _, err := d2compiler.Compile("", strings.NewReader(dsl), &d2compiler.CompileOptions{
|
||||
UTF16Pos: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, &WASMError{Message: err.Error(), Code: 400}
|
||||
}
|
||||
|
||||
objOrder, err := d2oracle.GetObjOrder(g, nil)
|
||||
if err != nil {
|
||||
return nil, &WASMError{Message: err.Error(), Code: 500}
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"order": objOrder,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func GetRefRanges(args []js.Value) (interface{}, error) {
|
||||
if len(args) < 4 {
|
||||
return nil, &WASMError{Message: "missing required arguments", Code: 400}
|
||||
}
|
||||
|
||||
var fs map[string]string
|
||||
if err := json.Unmarshal([]byte(args[0].String()), &fs); err != nil {
|
||||
return nil, &WASMError{Message: "invalid fs argument", Code: 400}
|
||||
}
|
||||
|
||||
file := args[1].String()
|
||||
key := args[2].String()
|
||||
|
||||
var boardPath []string
|
||||
if err := json.Unmarshal([]byte(args[3].String()), &boardPath); err != nil {
|
||||
return nil, &WASMError{Message: "invalid boardPath argument", Code: 400}
|
||||
}
|
||||
|
||||
ranges, importRanges, err := d2lsp.GetRefRanges(file, fs, boardPath, key)
|
||||
if err != nil {
|
||||
return nil, &WASMError{Message: err.Error(), Code: 500}
|
||||
}
|
||||
|
||||
return RefRangesResponse{
|
||||
Ranges: ranges,
|
||||
ImportRanges: importRanges,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func GetELKGraph(args []js.Value) (interface{}, error) {
|
||||
if len(args) < 1 {
|
||||
return nil, &WASMError{Message: "missing JSON argument", Code: 400}
|
||||
}
|
||||
var input CompileRequest
|
||||
if err := json.Unmarshal([]byte(args[0].String()), &input); err != nil {
|
||||
return nil, &WASMError{Message: "invalid JSON input", Code: 400}
|
||||
}
|
||||
|
||||
if input.FS == nil {
|
||||
return nil, &WASMError{Message: "missing 'fs' field in input JSON", Code: 400}
|
||||
}
|
||||
|
||||
if _, ok := input.FS["index"]; !ok {
|
||||
return nil, &WASMError{Message: "missing 'index' file in input fs", Code: 400}
|
||||
}
|
||||
|
||||
fs, err := memfs.New(input.FS)
|
||||
if err != nil {
|
||||
return nil, &WASMError{Message: fmt.Sprintf("invalid fs input: %s", err.Error()), Code: 400}
|
||||
}
|
||||
|
||||
g, _, err := d2compiler.Compile("", strings.NewReader(input.FS["index"]), &d2compiler.CompileOptions{
|
||||
UTF16Pos: true,
|
||||
FS: fs,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, &WASMError{Message: err.Error(), Code: 400}
|
||||
}
|
||||
|
||||
ruler, err := textmeasure.NewRuler()
|
||||
if err != nil {
|
||||
return nil, &WASMError{Message: fmt.Sprintf("text ruler cannot be initialized: %s", err.Error()), Code: 500}
|
||||
}
|
||||
err = g.SetDimensions(nil, ruler, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
elk, err := d2elklayout.ConvertGraph(context.Background(), g, nil)
|
||||
if err != nil {
|
||||
return nil, &WASMError{Message: err.Error(), Code: 400}
|
||||
}
|
||||
return elk, nil
|
||||
}
|
||||
|
||||
func Compile(args []js.Value) (interface{}, error) {
|
||||
if len(args) < 1 {
|
||||
return nil, &WASMError{Message: "missing JSON argument", Code: 400}
|
||||
}
|
||||
var input CompileRequest
|
||||
if err := json.Unmarshal([]byte(args[0].String()), &input); err != nil {
|
||||
return nil, &WASMError{Message: "invalid JSON input", Code: 400}
|
||||
}
|
||||
|
||||
if input.FS == nil {
|
||||
return nil, &WASMError{Message: "missing 'fs' field in input JSON", Code: 400}
|
||||
}
|
||||
|
||||
if _, ok := input.FS["index"]; !ok {
|
||||
return nil, &WASMError{Message: "missing 'index' file in input fs", Code: 400}
|
||||
}
|
||||
|
||||
fs, err := memfs.New(input.FS)
|
||||
if err != nil {
|
||||
return nil, &WASMError{Message: fmt.Sprintf("invalid fs input: %s", err.Error()), Code: 400}
|
||||
}
|
||||
|
||||
ruler, err := textmeasure.NewRuler()
|
||||
if err != nil {
|
||||
return nil, &WASMError{Message: fmt.Sprintf("text ruler cannot be initialized: %s", err.Error()), Code: 500}
|
||||
}
|
||||
ctx := log.WithDefault(context.Background())
|
||||
layoutFunc := d2dagrelayout.DefaultLayout
|
||||
if input.Opts != nil && input.Opts.Layout != nil {
|
||||
switch *input.Opts.Layout {
|
||||
case "dagre":
|
||||
layoutFunc = d2dagrelayout.DefaultLayout
|
||||
case "elk":
|
||||
layoutFunc = d2elklayout.DefaultLayout
|
||||
default:
|
||||
return nil, &WASMError{Message: fmt.Sprintf("layout option '%s' not recognized", *input.Opts.Layout), Code: 400}
|
||||
}
|
||||
}
|
||||
layoutResolver := func(engine string) (d2graph.LayoutGraph, error) {
|
||||
return layoutFunc, nil
|
||||
}
|
||||
|
||||
renderOpts := &d2svg.RenderOpts{}
|
||||
var fontFamily *d2fonts.FontFamily
|
||||
if input.Opts != nil && input.Opts.Sketch != nil && *input.Opts.Sketch {
|
||||
fontFamily = go2.Pointer(d2fonts.HandDrawn)
|
||||
renderOpts.Sketch = input.Opts.Sketch
|
||||
}
|
||||
if input.Opts != nil && input.Opts.ThemeID != nil {
|
||||
renderOpts.ThemeID = input.Opts.ThemeID
|
||||
}
|
||||
diagram, g, err := d2lib.Compile(ctx, input.FS["index"], &d2lib.CompileOptions{
|
||||
UTF16Pos: true,
|
||||
FS: fs,
|
||||
Ruler: ruler,
|
||||
LayoutResolver: layoutResolver,
|
||||
FontFamily: fontFamily,
|
||||
}, renderOpts)
|
||||
if err != nil {
|
||||
if pe, ok := err.(*d2parser.ParseError); ok {
|
||||
return nil, &WASMError{Message: pe.Error(), Code: 400}
|
||||
}
|
||||
return nil, &WASMError{Message: err.Error(), Code: 500}
|
||||
}
|
||||
|
||||
input.FS["index"] = d2format.Format(g.AST)
|
||||
|
||||
return CompileResponse{
|
||||
FS: input.FS,
|
||||
Diagram: *diagram,
|
||||
Graph: *g,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func Render(args []js.Value) (interface{}, error) {
|
||||
if len(args) < 1 {
|
||||
return nil, &WASMError{Message: "missing JSON argument", Code: 400}
|
||||
}
|
||||
var input RenderRequest
|
||||
if err := json.Unmarshal([]byte(args[0].String()), &input); err != nil {
|
||||
return nil, &WASMError{Message: "invalid JSON input", Code: 400}
|
||||
}
|
||||
|
||||
if input.Diagram == nil {
|
||||
return nil, &WASMError{Message: "missing 'diagram' field in input JSON", Code: 400}
|
||||
}
|
||||
|
||||
renderOpts := &d2svg.RenderOpts{}
|
||||
if input.Opts != nil && input.Opts.Sketch != nil {
|
||||
renderOpts.Sketch = input.Opts.Sketch
|
||||
}
|
||||
if input.Opts != nil && input.Opts.ThemeID != nil {
|
||||
renderOpts.ThemeID = input.Opts.ThemeID
|
||||
}
|
||||
out, err := d2svg.Render(input.Diagram, renderOpts)
|
||||
if err != nil {
|
||||
return nil, &WASMError{Message: fmt.Sprintf("render failed: %s", err.Error()), Code: 500}
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func GetBoardAtPosition(args []js.Value) (interface{}, error) {
|
||||
if len(args) < 3 {
|
||||
return nil, &WASMError{Message: "missing required arguments", Code: 400}
|
||||
}
|
||||
|
||||
dsl := args[0].String()
|
||||
line := args[1].Int()
|
||||
column := args[2].Int()
|
||||
|
||||
boardPath, err := d2lsp.GetBoardAtPosition(dsl, d2ast.Position{
|
||||
Line: line,
|
||||
Column: column,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, &WASMError{Message: err.Error(), Code: 500}
|
||||
}
|
||||
|
||||
return BoardPositionResponse{BoardPath: boardPath}, nil
|
||||
}
|
||||
|
||||
func Encode(args []js.Value) (interface{}, error) {
|
||||
if len(args) < 1 {
|
||||
return nil, &WASMError{Message: "missing script argument", Code: 400}
|
||||
}
|
||||
|
||||
script := args[0].String()
|
||||
encoded, err := urlenc.Encode(script)
|
||||
// should never happen
|
||||
if err != nil {
|
||||
return nil, &WASMError{Message: err.Error(), Code: 500}
|
||||
}
|
||||
|
||||
return map[string]string{"result": encoded}, nil
|
||||
}
|
||||
|
||||
func Decode(args []js.Value) (interface{}, error) {
|
||||
if len(args) < 1 {
|
||||
return nil, &WASMError{Message: "missing script argument", Code: 400}
|
||||
}
|
||||
|
||||
script := args[0].String()
|
||||
script, err := urlenc.Decode(script)
|
||||
if err != nil {
|
||||
return nil, &WASMError{Message: err.Error(), Code: 500}
|
||||
}
|
||||
return map[string]string{"result": script}, nil
|
||||
}
|
||||
|
||||
func GetVersion(args []js.Value) (interface{}, error) {
|
||||
return version.Version, nil
|
||||
}
|
||||
|
||||
func GetCompletions(args []js.Value) (interface{}, error) {
|
||||
if len(args) < 3 {
|
||||
return nil, &WASMError{Message: "missing required arguments", Code: 400}
|
||||
}
|
||||
|
||||
text := args[0].String()
|
||||
line := args[1].Int()
|
||||
column := args[2].Int()
|
||||
|
||||
completions, err := d2lsp.GetCompletionItems(text, line, column)
|
||||
if err != nil {
|
||||
return nil, &WASMError{Message: err.Error(), Code: 500}
|
||||
}
|
||||
|
||||
// Convert to map for JSON serialization
|
||||
items := make([]map[string]interface{}, len(completions))
|
||||
for i, completion := range completions {
|
||||
items[i] = map[string]interface{}{
|
||||
"label": completion.Label,
|
||||
"kind": int(completion.Kind),
|
||||
"detail": completion.Detail,
|
||||
"insertText": completion.InsertText,
|
||||
}
|
||||
}
|
||||
|
||||
return CompletionResponse{
|
||||
Items: items,
|
||||
}, nil
|
||||
}
|
||||
58
d2js/d2wasm/types.go
Normal file
58
d2js/d2wasm/types.go
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
//go:build js && wasm
|
||||
|
||||
package d2wasm
|
||||
|
||||
import (
|
||||
"oss.terrastruct.com/d2/d2ast"
|
||||
"oss.terrastruct.com/d2/d2graph"
|
||||
"oss.terrastruct.com/d2/d2target"
|
||||
)
|
||||
|
||||
type WASMResponse struct {
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
Error *WASMError `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type WASMError struct {
|
||||
Message string `json:"message"`
|
||||
Code int `json:"code"`
|
||||
}
|
||||
|
||||
func (e *WASMError) Error() string {
|
||||
return e.Message
|
||||
}
|
||||
|
||||
type RefRangesResponse struct {
|
||||
Ranges []d2ast.Range `json:"ranges"`
|
||||
ImportRanges []d2ast.Range `json:"importRanges"`
|
||||
}
|
||||
|
||||
type BoardPositionResponse struct {
|
||||
BoardPath []string `json:"boardPath"`
|
||||
}
|
||||
|
||||
type CompileRequest struct {
|
||||
FS map[string]string `json:"fs"`
|
||||
Opts *RenderOptions `json:"options"`
|
||||
}
|
||||
|
||||
type RenderOptions struct {
|
||||
Layout *string `json:"layout"`
|
||||
Sketch *bool `json:"sketch"`
|
||||
ThemeID *int64 `json:"themeID"`
|
||||
}
|
||||
|
||||
type CompileResponse struct {
|
||||
FS map[string]string `json:"fs"`
|
||||
Diagram d2target.Diagram `json:"diagram"`
|
||||
Graph d2graph.Graph `json:"graph"`
|
||||
}
|
||||
|
||||
type CompletionResponse struct {
|
||||
Items []map[string]interface{} `json:"items"`
|
||||
}
|
||||
|
||||
type RenderRequest struct {
|
||||
Diagram *d2target.Diagram `json:"diagram"`
|
||||
Opts *RenderOptions `json:"options"`
|
||||
}
|
||||
32
d2js/js.go
Normal file
32
d2js/js.go
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
//go:build js && wasm
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"syscall/js"
|
||||
|
||||
"oss.terrastruct.com/d2/d2js/d2wasm"
|
||||
)
|
||||
|
||||
func main() {
|
||||
api := d2wasm.NewD2API()
|
||||
|
||||
api.Register("getCompletions", d2wasm.GetCompletions)
|
||||
api.Register("getParentID", d2wasm.GetParentID)
|
||||
api.Register("getObjOrder", d2wasm.GetObjOrder)
|
||||
api.Register("getRefRanges", d2wasm.GetRefRanges)
|
||||
api.Register("getELKGraph", d2wasm.GetELKGraph)
|
||||
api.Register("compile", d2wasm.Compile)
|
||||
api.Register("render", d2wasm.Render)
|
||||
api.Register("getBoardAtPosition", d2wasm.GetBoardAtPosition)
|
||||
api.Register("encode", d2wasm.Encode)
|
||||
api.Register("decode", d2wasm.Decode)
|
||||
api.Register("version", d2wasm.GetVersion)
|
||||
|
||||
api.ExportTo(js.Global())
|
||||
|
||||
if cb := js.Global().Get("onWasmInitialized"); !cb.IsUndefined() {
|
||||
cb.Invoke()
|
||||
}
|
||||
select {}
|
||||
}
|
||||
28
d2js/js/.gitignore
vendored
Normal file
28
d2js/js/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
node_modules
|
||||
.npm
|
||||
bun.lockb
|
||||
|
||||
src/wasm-loader.browser.js
|
||||
wasm/d2.wasm
|
||||
dist/
|
||||
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
coverage/
|
||||
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
*.tmp
|
||||
*.temp
|
||||
.cache/
|
||||
1
d2js/js/.prettierignore
Normal file
1
d2js/js/.prettierignore
Normal file
|
|
@ -0,0 +1 @@
|
|||
src/platform.browser.js
|
||||
8
d2js/js/CHANGELOG.md
Normal file
8
d2js/js/CHANGELOG.md
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# Changelog
|
||||
|
||||
All notable changes to only the d2.js package will be documented in this file. **Does not
|
||||
include changes to the main d2 project.**
|
||||
|
||||
## [0.1.0] - 2025-01-12
|
||||
|
||||
First public release
|
||||
29
d2js/js/Makefile
Normal file
29
d2js/js/Makefile
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
.POSIX:
|
||||
.PHONY: all
|
||||
all: fmt build test cleanup
|
||||
|
||||
.PHONY: fmt
|
||||
fmt: node_modules
|
||||
prefix "$@" ../../ci/sub/bin/fmt.sh
|
||||
prefix "$@" rm -f yarn.lock
|
||||
|
||||
.PHONY: build
|
||||
build: fmt
|
||||
prefix "$@" ./ci/build.sh
|
||||
|
||||
.PHONY: dev
|
||||
dev: build
|
||||
prefix "$@" git checkout -- src/platform.js src/worker.js
|
||||
prefix "$@" bun run dev
|
||||
|
||||
.PHONY: test
|
||||
test: build
|
||||
prefix "$@" bun test:all
|
||||
|
||||
.PHONY: node_modules
|
||||
node_modules:
|
||||
prefix "$@" bun install $${CI:+--frozen-lockfile}
|
||||
|
||||
.PHONY: cleanup
|
||||
cleanup: test
|
||||
prefix "$@" git checkout -- src/platform.js src/worker.js
|
||||
96
d2js/js/README.md
Normal file
96
d2js/js/README.md
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
# D2.js
|
||||
|
||||
[](https://www.npmjs.com/package/@terrastruct/d2)
|
||||
[](https://mozilla.org/MPL/2.0/)
|
||||
|
||||
D2.js is a JavaScript wrapper around D2, the modern diagram scripting language. It enables running D2 directly in browsers and Node environments through WebAssembly.
|
||||
|
||||
## Features
|
||||
|
||||
- 🌐 **Universal** - Works in both browser and Node environments
|
||||
- 🚀 **Modern** - Built with ESM modules, with CJS fallback
|
||||
- 🔄 **Isomorphic** - Same API everywhere
|
||||
- ⚡ **Fast** - Powered by WebAssembly for near-native performance
|
||||
- 📦 **Lightweight** - Minimal wrapper around the core D2 engine
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm install @terrastruct/d2
|
||||
|
||||
# yarn
|
||||
yarn add @terrastruct/d2
|
||||
|
||||
# pnpm
|
||||
pnpm add @terrastruct/d2
|
||||
|
||||
# bun
|
||||
bun add @terrastruct/d2
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
D2.js uses webworkers to call a WASM file.
|
||||
|
||||
```javascript
|
||||
// Same for Node or browser
|
||||
import { D2 } from '@terrastruct/d2';
|
||||
// Or using a CDN
|
||||
// import { D2 } from 'https://esm.sh/@terrastruct/d2';
|
||||
|
||||
const d2 = new D2();
|
||||
|
||||
const result = await d2.compile('x -> y');
|
||||
const svg = await d2.render(result.diagram);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `new D2()`
|
||||
Creates a new D2 instance.
|
||||
|
||||
### `compile(input: string, options?: CompileOptions): Promise<CompileResult>`
|
||||
Compiles D2 markup into an intermediate representation.
|
||||
|
||||
Options:
|
||||
- `layout`: Layout engine to use ('dagre' | 'elk') [default: 'dagre']
|
||||
- `sketch`: Enable sketch mode [default: false]
|
||||
|
||||
### `render(diagram: Diagram, options?: RenderOptions): Promise<string>`
|
||||
Renders a compiled diagram to SVG.
|
||||
|
||||
## Development
|
||||
|
||||
D2.js uses Bun, so install this first.
|
||||
|
||||
### Building from source
|
||||
|
||||
```bash
|
||||
git clone https://github.com/terrastruct/d2.git
|
||||
cd d2/d2js/js
|
||||
./make.sh all
|
||||
```
|
||||
|
||||
If you change the main D2 source code, you should regenerate the WASM file:
|
||||
```bash
|
||||
./make.sh build
|
||||
```
|
||||
|
||||
### Running the dev server
|
||||
|
||||
You can browse the examples by running the dev server:
|
||||
|
||||
```bash
|
||||
./make.sh dev
|
||||
```
|
||||
|
||||
Visit `http://localhost:3000` to see the example page.
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome!
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the Mozilla Public License Version 2.0.
|
||||
107
d2js/js/build.js
Normal file
107
d2js/js/build.js
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
import { build } from "bun";
|
||||
import { copyFile, mkdir, writeFile, readFile, rm } from "node:fs/promises";
|
||||
import { join, resolve } from "node:path";
|
||||
|
||||
const __dirname = new URL(".", import.meta.url).pathname;
|
||||
const ROOT_DIR = resolve(__dirname);
|
||||
const SRC_DIR = resolve(ROOT_DIR, "src");
|
||||
|
||||
await rm("./dist", { recursive: true, force: true });
|
||||
await mkdir("./dist/browser", { recursive: true });
|
||||
await mkdir("./dist/node-esm", { recursive: true });
|
||||
await mkdir("./dist/node-cjs", { recursive: true });
|
||||
|
||||
const wasmBinary = await readFile("./wasm/d2.wasm");
|
||||
const wasmExecJs = await readFile("./wasm/wasm_exec.js", "utf8");
|
||||
|
||||
await writeFile(
|
||||
join(SRC_DIR, "wasm-loader.browser.js"),
|
||||
`export const wasmBinary = Uint8Array.from(atob("${Buffer.from(wasmBinary).toString(
|
||||
"base64"
|
||||
)}"), c => c.charCodeAt(0));
|
||||
export const wasmExecJs = ${JSON.stringify(wasmExecJs)};`
|
||||
);
|
||||
|
||||
const commonConfig = {
|
||||
minify: true,
|
||||
};
|
||||
|
||||
async function buildDynamicFiles(platform) {
|
||||
const platformContent =
|
||||
platform === "node"
|
||||
? `export * from "./platform.node.js";`
|
||||
: `export * from "./platform.browser.js";`;
|
||||
|
||||
const platformPath = join(SRC_DIR, "platform.js");
|
||||
await writeFile(platformPath, platformContent);
|
||||
|
||||
const workerSource =
|
||||
platform === "node"
|
||||
? join(SRC_DIR, "worker.node.js")
|
||||
: join(SRC_DIR, "worker.browser.js");
|
||||
|
||||
const workerTarget = join(SRC_DIR, "worker.js");
|
||||
const workerContent = await readFile(workerSource, "utf8");
|
||||
await writeFile(workerTarget, workerContent);
|
||||
}
|
||||
|
||||
async function buildAndCopy(buildType) {
|
||||
const configs = {
|
||||
browser: {
|
||||
outdir: resolve(ROOT_DIR, "dist/browser"),
|
||||
splitting: false,
|
||||
format: "esm",
|
||||
target: "browser",
|
||||
platform: "browser",
|
||||
entrypoints: [resolve(SRC_DIR, "index.js")],
|
||||
},
|
||||
"node-esm": {
|
||||
outdir: resolve(ROOT_DIR, "dist/node-esm"),
|
||||
splitting: true,
|
||||
format: "esm",
|
||||
target: "node",
|
||||
platform: "node",
|
||||
entrypoints: [resolve(SRC_DIR, "index.js"), resolve(SRC_DIR, "worker.js")],
|
||||
},
|
||||
"node-cjs": {
|
||||
outdir: resolve(ROOT_DIR, "dist/node-cjs"),
|
||||
splitting: false,
|
||||
format: "cjs",
|
||||
target: "node",
|
||||
platform: "node",
|
||||
entrypoints: [resolve(SRC_DIR, "index.js"), resolve(SRC_DIR, "worker.js")],
|
||||
},
|
||||
};
|
||||
|
||||
const config = configs[buildType];
|
||||
await buildDynamicFiles(config.platform);
|
||||
|
||||
const result = await build({
|
||||
...commonConfig,
|
||||
...config,
|
||||
});
|
||||
|
||||
if (!result.outputs || result.outputs.length === 0) {
|
||||
throw new Error(
|
||||
`No outputs generated for ${buildType} build. Result: ${JSON.stringify(result)}`
|
||||
);
|
||||
}
|
||||
|
||||
if (buildType !== "browser") {
|
||||
await copyFile(resolve(ROOT_DIR, "wasm/d2.wasm"), join(config.outdir, "d2.wasm"));
|
||||
await copyFile(
|
||||
resolve(ROOT_DIR, "wasm/wasm_exec.js"),
|
||||
join(config.outdir, "wasm_exec.js")
|
||||
);
|
||||
await copyFile(resolve(ROOT_DIR, "src/elk.js"), join(config.outdir, "elk.js"));
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await buildAndCopy("browser");
|
||||
await buildAndCopy("node-esm");
|
||||
await buildAndCopy("node-cjs");
|
||||
} catch (error) {
|
||||
console.error("Build failed:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
BIN
d2js/js/bun.lockb
Executable file
BIN
d2js/js/bun.lockb
Executable file
Binary file not shown.
19
d2js/js/ci/build.sh
Executable file
19
d2js/js/ci/build.sh
Executable file
|
|
@ -0,0 +1,19 @@
|
|||
#!/bin/sh
|
||||
set -eu
|
||||
. "$(dirname "$0")/../../../ci/sub/lib.sh"
|
||||
cd -- "$(dirname "$0")/.."
|
||||
|
||||
cd ../..
|
||||
sh_c "GOOS=js GOARCH=wasm go build -ldflags='-s -w' -trimpath -o main.wasm ./d2js"
|
||||
sh_c "mv main.wasm ./d2js/js/wasm/d2.wasm"
|
||||
|
||||
if [ ! -f ./d2js/js/wasm/d2.wasm ]; then
|
||||
echoerr "Error: d2.wasm is missing"
|
||||
exit 1
|
||||
else
|
||||
echo "d2.wasm exists. Size:"
|
||||
ls -lh ./d2js/js/wasm/d2.wasm | awk '{print $5}'
|
||||
fi
|
||||
|
||||
cd d2js/js
|
||||
sh_c bun build.js
|
||||
72
d2js/js/dev-server.js
Normal file
72
d2js/js/dev-server.js
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
const fs = require("fs/promises");
|
||||
const path = require("path");
|
||||
|
||||
const MIME_TYPES = {
|
||||
".html": "text/html",
|
||||
".js": "text/javascript",
|
||||
".mjs": "text/javascript",
|
||||
".css": "text/css",
|
||||
".wasm": "application/wasm",
|
||||
".svg": "image/svg+xml",
|
||||
};
|
||||
|
||||
const server = Bun.serve({
|
||||
port: 3000,
|
||||
async fetch(request) {
|
||||
const url = new URL(request.url);
|
||||
let filePath = url.pathname.slice(1); // Remove leading "/"
|
||||
|
||||
if (filePath === "") {
|
||||
filePath = "examples/";
|
||||
}
|
||||
|
||||
try {
|
||||
const fullPath = path.join(process.cwd(), filePath);
|
||||
const stats = await fs.stat(fullPath);
|
||||
|
||||
if (stats.isDirectory()) {
|
||||
const entries = await fs.readdir(fullPath);
|
||||
const links = await Promise.all(
|
||||
entries.map(async (entry) => {
|
||||
const entryPath = path.join(fullPath, entry);
|
||||
const isDir = (await fs.stat(entryPath)).isDirectory();
|
||||
const slash = isDir ? "/" : "";
|
||||
return `<li><a href="${filePath}${entry}${slash}">${entry}${slash}</a></li>`;
|
||||
})
|
||||
);
|
||||
|
||||
const html = `
|
||||
<html>
|
||||
<body>
|
||||
<h1>Examples</h1>
|
||||
<ul>
|
||||
${links.join("")}
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
return new Response(html, {
|
||||
headers: { "Content-Type": "text/html" },
|
||||
});
|
||||
} else {
|
||||
const ext = path.extname(filePath);
|
||||
const mimeType = MIME_TYPES[ext] || "application/octet-stream";
|
||||
|
||||
const file = Bun.file(filePath);
|
||||
return new Response(file, {
|
||||
headers: {
|
||||
"Content-Type": mimeType,
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Cross-Origin-Opener-Policy": "same-origin",
|
||||
"Cross-Origin-Embedder-Policy": "require-corp",
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Error serving ${filePath}:`, err);
|
||||
return new Response(`File not found: ${filePath}`, { status: 404 });
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`Server running at http://localhost:3000`);
|
||||
49
d2js/js/examples/basic.html
Normal file
49
d2js/js/examples/basic.html
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
padding: 20px;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
}
|
||||
textarea {
|
||||
width: 400px;
|
||||
height: 300px;
|
||||
}
|
||||
#output {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
#output svg {
|
||||
max-width: 100%;
|
||||
max-height: 90vh;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
<textarea id="input">x -> y</textarea>
|
||||
<button onclick="compile()">Compile</button>
|
||||
</div>
|
||||
<div id="output"></div>
|
||||
<script type="module">
|
||||
import { D2 } from "../dist/browser/index.js";
|
||||
const d2 = new D2();
|
||||
window.compile = async () => {
|
||||
const input = document.getElementById("input").value;
|
||||
try {
|
||||
const result = await d2.compile(input);
|
||||
const svg = await d2.render(result.diagram);
|
||||
document.getElementById("output").innerHTML = svg;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
document.getElementById("output").textContent = err.message;
|
||||
}
|
||||
};
|
||||
compile();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
122
d2js/js/examples/customizable.html
Normal file
122
d2js/js/examples/customizable.html
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
padding: 20px;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
}
|
||||
.controls {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
width: 400px;
|
||||
}
|
||||
textarea {
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
padding: 8px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
}
|
||||
.options-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.layout-toggle,
|
||||
.sketch-toggle {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
}
|
||||
.radio-group {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
.radio-label,
|
||||
.checkbox-label {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
button {
|
||||
padding: 8px 16px;
|
||||
background: #0066cc;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
button:hover {
|
||||
background: #0052a3;
|
||||
}
|
||||
#output {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 4px;
|
||||
padding: 16px;
|
||||
}
|
||||
#output svg {
|
||||
max-width: 100%;
|
||||
max-height: 90vh;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="controls">
|
||||
<textarea id="input">x -> y</textarea>
|
||||
<div class="options-group">
|
||||
<div class="layout-toggle">
|
||||
<span>Layout:</span>
|
||||
<div class="radio-group">
|
||||
<label class="radio-label">
|
||||
<input type="radio" name="layout" value="dagre" checked />
|
||||
Dagre
|
||||
</label>
|
||||
<label class="radio-label">
|
||||
<input type="radio" name="layout" value="elk" />
|
||||
ELK
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sketch-toggle">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="sketch" />
|
||||
Sketch mode
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<button onclick="compile()">Compile</button>
|
||||
</div>
|
||||
<div id="output"></div>
|
||||
<script type="module">
|
||||
import { D2 } from "../dist/browser/index.js";
|
||||
const d2 = new D2();
|
||||
window.compile = async () => {
|
||||
const input = document.getElementById("input").value;
|
||||
const layout = document.querySelector('input[name="layout"]:checked').value;
|
||||
const sketch = document.getElementById("sketch").checked;
|
||||
try {
|
||||
const result = await d2.compile(input, { layout, sketch });
|
||||
const svg = await d2.render(result.diagram, { sketch });
|
||||
document.getElementById("output").innerHTML = svg;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
document.getElementById("output").textContent = err.message;
|
||||
}
|
||||
};
|
||||
compile();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
23
d2js/js/make.sh
Executable file
23
d2js/js/make.sh
Executable file
|
|
@ -0,0 +1,23 @@
|
|||
#!/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"
|
||||
PATH="$(cd -- "$(dirname "$0")" && pwd)/../../ci/sub/bin:$PATH"
|
||||
cd -- "$(dirname "$0")"
|
||||
|
||||
if ! command -v bun >/dev/null 2>&1; then
|
||||
if [ -n "${CI-}" ]; then
|
||||
echo "Bun is not installed. Installing Bun..."
|
||||
curl -fsSL https://bun.sh/install | bash
|
||||
export PATH="$HOME/.bun/bin:$PATH"
|
||||
else
|
||||
echoerr "You need bun to build d2.js: curl -fsSL https://bun.sh/install | bash"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
_make "$@"
|
||||
56
d2js/js/package.json
Normal file
56
d2js/js/package.json
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
{
|
||||
"name": "@terrastruct/d2",
|
||||
"author": "Terrastruct, Inc.",
|
||||
"description": "D2.js is a wrapper around the WASM build of D2, the modern text-to-diagram language.",
|
||||
"version": "0.1.21",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/terrastruct/d2.git",
|
||||
"directory": "d2js/js"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/terrastruct/d2/issues"
|
||||
},
|
||||
"homepage": "https://github.com/terrastruct/d2/tree/master/d2js/js#readme",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"type": "module",
|
||||
"main": "./dist/cjs/index.js",
|
||||
"module": "./dist/esm/index.js",
|
||||
"exports": {
|
||||
".": {
|
||||
"browser": "./dist/browser/index.js",
|
||||
"import": {
|
||||
"browser": "./dist/browser/index.js",
|
||||
"default": "./dist/node-esm/index.js"
|
||||
},
|
||||
"require": "./dist/node-cjs/index.js",
|
||||
"default": "./dist/node-esm/index.js"
|
||||
},
|
||||
"./worker": "./dist/browser/worker.js"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "./make.sh build",
|
||||
"test": "bun test test/unit",
|
||||
"test:integration": "bun test test/integration",
|
||||
"test:all": "bun run test && bun run test:integration",
|
||||
"dev": "bun --watch dev-server.js",
|
||||
"prepublishOnly": "./make.sh all"
|
||||
},
|
||||
"keywords": [
|
||||
"d2",
|
||||
"d2lang",
|
||||
"diagram",
|
||||
"wasm",
|
||||
"text-to-diagram",
|
||||
"go"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"devDependencies": {
|
||||
"bun": "latest"
|
||||
}
|
||||
}
|
||||
105805
d2js/js/src/elk.js
Normal file
105805
d2js/js/src/elk.js
Normal file
File diff suppressed because it is too large
Load diff
109
d2js/js/src/index.js
Normal file
109
d2js/js/src/index.js
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
import { createWorker, loadFile } from "./platform.js";
|
||||
|
||||
const DEFAULT_OPTIONS = {
|
||||
layout: "dagre",
|
||||
sketch: false,
|
||||
};
|
||||
|
||||
export class D2 {
|
||||
constructor() {
|
||||
this.ready = this.init();
|
||||
}
|
||||
|
||||
setupMessageHandler() {
|
||||
const isNode = typeof window === "undefined";
|
||||
return new Promise((resolve, reject) => {
|
||||
if (isNode) {
|
||||
this.worker.on("message", (data) => {
|
||||
if (data.type === "ready") resolve();
|
||||
if (data.type === "error") reject(new Error(data.error));
|
||||
if (data.type === "result" && this.currentResolve) {
|
||||
this.currentResolve(data.data);
|
||||
}
|
||||
if (data.type === "error" && this.currentReject) {
|
||||
this.currentReject(new Error(data.error));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.worker.onmessage = (e) => {
|
||||
if (e.data.type === "ready") resolve();
|
||||
if (e.data.type === "error") reject(new Error(e.data.error));
|
||||
if (e.data.type === "result" && this.currentResolve) {
|
||||
this.currentResolve(e.data.data);
|
||||
}
|
||||
if (e.data.type === "error" && this.currentReject) {
|
||||
this.currentReject(new Error(e.data.error));
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async init() {
|
||||
this.worker = await createWorker();
|
||||
|
||||
const elkContent = await loadFile("./elk.js");
|
||||
const wasmExecContent = await loadFile("./wasm_exec.js");
|
||||
const wasmBinary = await loadFile("./d2.wasm");
|
||||
|
||||
const isNode = typeof window === "undefined";
|
||||
const messageHandler = this.setupMessageHandler();
|
||||
|
||||
if (isNode) {
|
||||
this.worker.on("error", (error) => {
|
||||
console.error("Worker (node) encountered an error:", error.message || error);
|
||||
});
|
||||
} else {
|
||||
this.worker.onerror = (error) => {
|
||||
console.error("Worker encountered an error:", error.message || error);
|
||||
};
|
||||
}
|
||||
|
||||
this.worker.postMessage({
|
||||
type: "init",
|
||||
data: {
|
||||
wasm: wasmBinary,
|
||||
wasmExecContent: isNode ? wasmExecContent.toString() : null,
|
||||
elkContent: isNode ? elkContent.toString() : null,
|
||||
wasmExecUrl: isNode
|
||||
? null
|
||||
: URL.createObjectURL(
|
||||
new Blob([wasmExecContent], { type: "application/javascript" })
|
||||
),
|
||||
},
|
||||
});
|
||||
|
||||
return messageHandler;
|
||||
}
|
||||
|
||||
async sendMessage(type, data) {
|
||||
await this.ready;
|
||||
return new Promise((resolve, reject) => {
|
||||
this.currentResolve = resolve;
|
||||
this.currentReject = reject;
|
||||
this.worker.postMessage({ type, data });
|
||||
});
|
||||
}
|
||||
|
||||
async compile(input, options = {}) {
|
||||
const opts = { ...DEFAULT_OPTIONS, ...options };
|
||||
const request =
|
||||
typeof input === "string"
|
||||
? { fs: { index: input }, options: opts }
|
||||
: { ...input, options: { ...opts, ...input.options } };
|
||||
return this.sendMessage("compile", request);
|
||||
}
|
||||
|
||||
async render(diagram, options = {}) {
|
||||
const opts = { ...DEFAULT_OPTIONS, ...options };
|
||||
return this.sendMessage("render", { diagram, options: opts });
|
||||
}
|
||||
|
||||
async encode(script) {
|
||||
return this.sendMessage("encode", script);
|
||||
}
|
||||
|
||||
async decode(encoded) {
|
||||
return this.sendMessage("decode", encoded);
|
||||
}
|
||||
}
|
||||
26
d2js/js/src/platform.browser.js
Normal file
26
d2js/js/src/platform.browser.js
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { wasmBinary, wasmExecJs } from "./wasm-loader.browser.js";
|
||||
import workerScript from "./worker.js" with { type: "text" };
|
||||
import elkScript from "./elk.js" with { type: "text" };
|
||||
|
||||
// For the browser version, we build the wasm files into a file (wasm-loader.browser.js)
|
||||
// and loading a file just reads the text, so there's no external dependency calls
|
||||
export async function loadFile(path) {
|
||||
if (path === "./d2.wasm") {
|
||||
return wasmBinary.buffer;
|
||||
}
|
||||
if (path === "./wasm_exec.js") {
|
||||
return new TextEncoder().encode(wasmExecJs).buffer;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function createWorker() {
|
||||
let blob = new Blob([wasmExecJs, elkScript, workerScript], {
|
||||
type: "text/javascript;charset=utf-8",
|
||||
});
|
||||
|
||||
const worker = new Worker(URL.createObjectURL(blob), {
|
||||
type: "module",
|
||||
});
|
||||
return worker;
|
||||
}
|
||||
1
d2js/js/src/platform.js
Normal file
1
d2js/js/src/platform.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from "./platform.node.js";
|
||||
40
d2js/js/src/platform.node.js
Normal file
40
d2js/js/src/platform.node.js
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
let nodeModules = null;
|
||||
|
||||
async function loadNodeModules() {
|
||||
if (!nodeModules) {
|
||||
nodeModules = {
|
||||
fs: await import("fs/promises"),
|
||||
path: await import("path"),
|
||||
url: await import("url"),
|
||||
worker: await import("worker_threads"),
|
||||
};
|
||||
}
|
||||
return nodeModules;
|
||||
}
|
||||
|
||||
export async function loadFile(path) {
|
||||
const modules = await loadNodeModules();
|
||||
const readFile = modules.fs.readFile;
|
||||
const { join, dirname } = modules.path;
|
||||
const { fileURLToPath } = modules.url;
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
try {
|
||||
return await readFile(join(__dirname, path));
|
||||
} catch (err) {
|
||||
if (err.code === "ENOENT") {
|
||||
return await readFile(join(__dirname, "../../../wasm", path.replace("./", "")));
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
export async function createWorker() {
|
||||
const modules = await loadNodeModules();
|
||||
const { Worker } = modules.worker;
|
||||
const { join, dirname } = modules.path;
|
||||
const { fileURLToPath } = modules.url;
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const workerPath = join(__dirname, "worker.js");
|
||||
return new Worker(workerPath);
|
||||
}
|
||||
8
d2js/js/src/wasm-loader.node.js
Normal file
8
d2js/js/src/wasm-loader.node.js
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { readFile } from "fs/promises";
|
||||
import { fileURLToPath } from "url";
|
||||
import { dirname, resolve } from "path";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
export async function getWasmBinary() {
|
||||
return readFile(resolve(__dirname, "./d2.wasm"));
|
||||
}
|
||||
76
d2js/js/src/worker.browser.js
Normal file
76
d2js/js/src/worker.browser.js
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
let currentPort;
|
||||
let d2;
|
||||
let elk;
|
||||
|
||||
export function setupMessageHandler(isNode, port, initWasm) {
|
||||
currentPort = port;
|
||||
|
||||
const handleMessage = async (e) => {
|
||||
const { type, data } = e;
|
||||
|
||||
switch (type) {
|
||||
case "init":
|
||||
try {
|
||||
if (isNode) {
|
||||
eval(data.wasmExecContent);
|
||||
}
|
||||
d2 = await initWasm(data.wasm);
|
||||
elk = new ELK();
|
||||
currentPort.postMessage({ type: "ready" });
|
||||
} catch (err) {
|
||||
currentPort.postMessage({ type: "error", error: err.message });
|
||||
}
|
||||
break;
|
||||
|
||||
case "compile":
|
||||
try {
|
||||
// We use Go to get the intermediate ELK graph
|
||||
// Then natively run elk layout
|
||||
// This is due to elk.layout being an async function, which a
|
||||
// single-threaded WASM call cannot complete without giving control back
|
||||
// So we compute it, store it here, then during elk layout, instead
|
||||
// of computing again, we use this variable (and unset it for next call)
|
||||
if (data.options.layout === "elk") {
|
||||
const elkGraph = await d2.getELKGraph(JSON.stringify(data));
|
||||
const elkGraph2 = JSON.parse(elkGraph).data;
|
||||
const layout = await elk.layout(elkGraph2);
|
||||
globalThis.elkResult = layout;
|
||||
}
|
||||
|
||||
const result = await d2.compile(JSON.stringify(data));
|
||||
const response = JSON.parse(result);
|
||||
if (response.error) throw new Error(response.error.message);
|
||||
currentPort.postMessage({ type: "result", data: response.data });
|
||||
} catch (err) {
|
||||
currentPort.postMessage({ type: "error", error: err.message });
|
||||
}
|
||||
break;
|
||||
|
||||
case "render":
|
||||
try {
|
||||
const result = await d2.render(JSON.stringify(data));
|
||||
const response = JSON.parse(result);
|
||||
if (response.error) throw new Error(response.error.message);
|
||||
currentPort.postMessage({ type: "result", data: atob(response.data) });
|
||||
} catch (err) {
|
||||
currentPort.postMessage({ type: "error", error: err.message });
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
if (isNode) {
|
||||
port.on("message", handleMessage);
|
||||
} else {
|
||||
port.onmessage = (e) => handleMessage(e.data);
|
||||
}
|
||||
}
|
||||
|
||||
async function initWasmBrowser(wasmBinary) {
|
||||
const go = new Go();
|
||||
const result = await WebAssembly.instantiate(wasmBinary, go.importObject);
|
||||
go.run(result.instance);
|
||||
return self.d2;
|
||||
}
|
||||
|
||||
setupMessageHandler(false, self, initWasmBrowser);
|
||||
72
d2js/js/src/worker.js
Normal file
72
d2js/js/src/worker.js
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import { parentPort } from "node:worker_threads";
|
||||
|
||||
let currentPort;
|
||||
let d2;
|
||||
let elk;
|
||||
|
||||
export function setupMessageHandler(isNode, port, initWasm) {
|
||||
currentPort = port;
|
||||
|
||||
const handleMessage = async (e) => {
|
||||
const { type, data } = e;
|
||||
|
||||
switch (type) {
|
||||
case "init":
|
||||
try {
|
||||
if (isNode) {
|
||||
eval(data.wasmExecContent);
|
||||
eval(data.elkContent);
|
||||
}
|
||||
d2 = await initWasm(data.wasm);
|
||||
elk = new ELK();
|
||||
currentPort.postMessage({ type: "ready" });
|
||||
} catch (err) {
|
||||
currentPort.postMessage({ type: "error", error: err.message });
|
||||
}
|
||||
break;
|
||||
|
||||
case "compile":
|
||||
try {
|
||||
if (data.options.layout === "elk") {
|
||||
const elkGraph = await d2.getELKGraph(JSON.stringify(data));
|
||||
const elkGraph2 = JSON.parse(elkGraph).data;
|
||||
const layout = await elk.layout(elkGraph2);
|
||||
globalThis.elkResult = layout;
|
||||
}
|
||||
const result = await d2.compile(JSON.stringify(data));
|
||||
const response = JSON.parse(result);
|
||||
if (response.error) throw new Error(response.error.message);
|
||||
currentPort.postMessage({ type: "result", data: response.data });
|
||||
} catch (err) {
|
||||
currentPort.postMessage({ type: "error", error: err.message });
|
||||
}
|
||||
break;
|
||||
|
||||
case "render":
|
||||
try {
|
||||
const result = await d2.render(JSON.stringify(data));
|
||||
const response = JSON.parse(result);
|
||||
if (response.error) throw new Error(response.error.message);
|
||||
currentPort.postMessage({ type: "result", data: atob(response.data) });
|
||||
} catch (err) {
|
||||
currentPort.postMessage({ type: "error", error: err.message });
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
if (isNode) {
|
||||
port.on("message", handleMessage);
|
||||
} else {
|
||||
port.onmessage = (e) => handleMessage(e.data);
|
||||
}
|
||||
}
|
||||
|
||||
async function initWasmNode(wasmBinary) {
|
||||
const go = new Go();
|
||||
const result = await WebAssembly.instantiate(wasmBinary, go.importObject);
|
||||
go.run(result.instance);
|
||||
return global.d2;
|
||||
}
|
||||
|
||||
setupMessageHandler(true, parentPort, initWasmNode);
|
||||
72
d2js/js/src/worker.node.js
Normal file
72
d2js/js/src/worker.node.js
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import { parentPort } from "node:worker_threads";
|
||||
|
||||
let currentPort;
|
||||
let d2;
|
||||
let elk;
|
||||
|
||||
export function setupMessageHandler(isNode, port, initWasm) {
|
||||
currentPort = port;
|
||||
|
||||
const handleMessage = async (e) => {
|
||||
const { type, data } = e;
|
||||
|
||||
switch (type) {
|
||||
case "init":
|
||||
try {
|
||||
if (isNode) {
|
||||
eval(data.wasmExecContent);
|
||||
eval(data.elkContent);
|
||||
}
|
||||
d2 = await initWasm(data.wasm);
|
||||
elk = new ELK();
|
||||
currentPort.postMessage({ type: "ready" });
|
||||
} catch (err) {
|
||||
currentPort.postMessage({ type: "error", error: err.message });
|
||||
}
|
||||
break;
|
||||
|
||||
case "compile":
|
||||
try {
|
||||
if (data.options.layout === "elk") {
|
||||
const elkGraph = await d2.getELKGraph(JSON.stringify(data));
|
||||
const elkGraph2 = JSON.parse(elkGraph).data;
|
||||
const layout = await elk.layout(elkGraph2);
|
||||
globalThis.elkResult = layout;
|
||||
}
|
||||
const result = await d2.compile(JSON.stringify(data));
|
||||
const response = JSON.parse(result);
|
||||
if (response.error) throw new Error(response.error.message);
|
||||
currentPort.postMessage({ type: "result", data: response.data });
|
||||
} catch (err) {
|
||||
currentPort.postMessage({ type: "error", error: err.message });
|
||||
}
|
||||
break;
|
||||
|
||||
case "render":
|
||||
try {
|
||||
const result = await d2.render(JSON.stringify(data));
|
||||
const response = JSON.parse(result);
|
||||
if (response.error) throw new Error(response.error.message);
|
||||
currentPort.postMessage({ type: "result", data: atob(response.data) });
|
||||
} catch (err) {
|
||||
currentPort.postMessage({ type: "error", error: err.message });
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
if (isNode) {
|
||||
port.on("message", handleMessage);
|
||||
} else {
|
||||
port.onmessage = (e) => handleMessage(e.data);
|
||||
}
|
||||
}
|
||||
|
||||
async function initWasmNode(wasmBinary) {
|
||||
const go = new Go();
|
||||
const result = await WebAssembly.instantiate(wasmBinary, go.importObject);
|
||||
go.run(result.instance);
|
||||
return global.d2;
|
||||
}
|
||||
|
||||
setupMessageHandler(true, parentPort, initWasmNode);
|
||||
11
d2js/js/test/integration/cjs.test.js
Normal file
11
d2js/js/test/integration/cjs.test.js
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { expect, test, describe } from "bun:test";
|
||||
|
||||
describe("D2 CJS Integration", () => {
|
||||
test("can require and use CJS build", async () => {
|
||||
const { D2 } = require("../../dist/node-cjs/index.js");
|
||||
const d2 = new D2();
|
||||
const result = await d2.compile("x -> y");
|
||||
expect(result.diagram).toBeDefined();
|
||||
await d2.worker.terminate();
|
||||
}, 20000);
|
||||
});
|
||||
11
d2js/js/test/integration/esm.test.js
Normal file
11
d2js/js/test/integration/esm.test.js
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { expect, test, describe } from "bun:test";
|
||||
import { D2 } from "../../dist/node-esm/index.js";
|
||||
|
||||
describe("D2 ESM Integration", () => {
|
||||
test("can import and use ESM build", async () => {
|
||||
const d2 = new D2();
|
||||
const result = await d2.compile("x -> y");
|
||||
expect(result.diagram).toBeDefined();
|
||||
await d2.worker.terminate();
|
||||
}, 20000);
|
||||
});
|
||||
58
d2js/js/test/unit/basic.test.js
Normal file
58
d2js/js/test/unit/basic.test.js
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import { expect, test, describe } from "bun:test";
|
||||
import { D2 } from "../../dist/node-esm/index.js";
|
||||
|
||||
describe("D2 Unit Tests", () => {
|
||||
test("basic compilation works", async () => {
|
||||
const d2 = new D2();
|
||||
const result = await d2.compile("x -> y");
|
||||
expect(result.diagram).toBeDefined();
|
||||
await d2.worker.terminate();
|
||||
}, 20000);
|
||||
|
||||
test("elk layout works", async () => {
|
||||
const d2 = new D2();
|
||||
const result = await d2.compile("x -> y", { layout: "elk" });
|
||||
expect(result.diagram).toBeDefined();
|
||||
await d2.worker.terminate();
|
||||
}, 20000);
|
||||
|
||||
test("render works", async () => {
|
||||
const d2 = new D2();
|
||||
const result = await d2.compile("x -> y");
|
||||
const svg = await d2.render(result.diagram);
|
||||
expect(svg).toContain("<svg");
|
||||
expect(svg).toContain("</svg>");
|
||||
await d2.worker.terminate();
|
||||
}, 20000);
|
||||
|
||||
test("sketch render works", async () => {
|
||||
const d2 = new D2();
|
||||
const result = await d2.compile("x -> y", { sketch: true });
|
||||
const svg = await d2.render(result.diagram, { sketch: true });
|
||||
expect(svg).toContain("<svg");
|
||||
expect(svg).toContain("</svg>");
|
||||
expect(svg).toContain("sketch-overlay");
|
||||
await d2.worker.terminate();
|
||||
}, 20000);
|
||||
|
||||
test("latex works", async () => {
|
||||
const d2 = new D2();
|
||||
const result = await d2.compile("x: |latex \\frac{f(x+h)-f(x)}{h} |");
|
||||
const svg = await d2.render(result.diagram);
|
||||
expect(svg).toContain("<svg");
|
||||
expect(svg).toContain("</svg>");
|
||||
await d2.worker.terminate();
|
||||
}, 20000);
|
||||
|
||||
test("handles syntax errors correctly", async () => {
|
||||
const d2 = new D2();
|
||||
try {
|
||||
await d2.compile("invalid -> -> syntax");
|
||||
throw new Error("Should have thrown syntax error");
|
||||
} catch (err) {
|
||||
expect(err).toBeDefined();
|
||||
expect(err.message).not.toContain("Should have thrown syntax error");
|
||||
}
|
||||
await d2.worker.terminate();
|
||||
}, 20000);
|
||||
});
|
||||
477
d2js/js/wasm/wasm_exec.js
Normal file
477
d2js/js/wasm/wasm_exec.js
Normal file
|
|
@ -0,0 +1,477 @@
|
|||
"use strict";
|
||||
(() => {
|
||||
const o = () => {
|
||||
const h = new Error("not implemented");
|
||||
return (h.code = "ENOSYS"), h;
|
||||
};
|
||||
if (!globalThis.fs) {
|
||||
let h = "";
|
||||
globalThis.fs = {
|
||||
constants: {
|
||||
O_WRONLY: -1,
|
||||
O_RDWR: -1,
|
||||
O_CREAT: -1,
|
||||
O_TRUNC: -1,
|
||||
O_APPEND: -1,
|
||||
O_EXCL: -1,
|
||||
},
|
||||
writeSync(n, s) {
|
||||
h += y.decode(s);
|
||||
const i = h.lastIndexOf(`
|
||||
`);
|
||||
return (
|
||||
i != -1 && (console.log(h.substring(0, i)), (h = h.substring(i + 1))), s.length
|
||||
);
|
||||
},
|
||||
write(n, s, i, r, f, u) {
|
||||
if (i !== 0 || r !== s.length || f !== null) {
|
||||
u(o());
|
||||
return;
|
||||
}
|
||||
const d = this.writeSync(n, s);
|
||||
u(null, d);
|
||||
},
|
||||
chmod(n, s, i) {
|
||||
i(o());
|
||||
},
|
||||
chown(n, s, i, r) {
|
||||
r(o());
|
||||
},
|
||||
close(n, s) {
|
||||
s(o());
|
||||
},
|
||||
fchmod(n, s, i) {
|
||||
i(o());
|
||||
},
|
||||
fchown(n, s, i, r) {
|
||||
r(o());
|
||||
},
|
||||
fstat(n, s) {
|
||||
s(o());
|
||||
},
|
||||
fsync(n, s) {
|
||||
s(null);
|
||||
},
|
||||
ftruncate(n, s, i) {
|
||||
i(o());
|
||||
},
|
||||
lchown(n, s, i, r) {
|
||||
r(o());
|
||||
},
|
||||
link(n, s, i) {
|
||||
i(o());
|
||||
},
|
||||
lstat(n, s) {
|
||||
s(o());
|
||||
},
|
||||
mkdir(n, s, i) {
|
||||
i(o());
|
||||
},
|
||||
open(n, s, i, r) {
|
||||
r(o());
|
||||
},
|
||||
read(n, s, i, r, f, u) {
|
||||
u(o());
|
||||
},
|
||||
readdir(n, s) {
|
||||
s(o());
|
||||
},
|
||||
readlink(n, s) {
|
||||
s(o());
|
||||
},
|
||||
rename(n, s, i) {
|
||||
i(o());
|
||||
},
|
||||
rmdir(n, s) {
|
||||
s(o());
|
||||
},
|
||||
stat(n, s) {
|
||||
s(o());
|
||||
},
|
||||
symlink(n, s, i) {
|
||||
i(o());
|
||||
},
|
||||
truncate(n, s, i) {
|
||||
i(o());
|
||||
},
|
||||
unlink(n, s) {
|
||||
s(o());
|
||||
},
|
||||
utimes(n, s, i, r) {
|
||||
r(o());
|
||||
},
|
||||
};
|
||||
}
|
||||
if (
|
||||
(globalThis.process ||
|
||||
(globalThis.process = {
|
||||
getuid() {
|
||||
return -1;
|
||||
},
|
||||
getgid() {
|
||||
return -1;
|
||||
},
|
||||
geteuid() {
|
||||
return -1;
|
||||
},
|
||||
getegid() {
|
||||
return -1;
|
||||
},
|
||||
getgroups() {
|
||||
throw o();
|
||||
},
|
||||
pid: -1,
|
||||
ppid: -1,
|
||||
umask() {
|
||||
throw o();
|
||||
},
|
||||
cwd() {
|
||||
throw o();
|
||||
},
|
||||
chdir() {
|
||||
throw o();
|
||||
},
|
||||
}),
|
||||
!globalThis.crypto)
|
||||
)
|
||||
throw new Error(
|
||||
"globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)"
|
||||
);
|
||||
if (!globalThis.performance)
|
||||
throw new Error(
|
||||
"globalThis.performance is not available, polyfill required (performance.now only)"
|
||||
);
|
||||
if (!globalThis.TextEncoder)
|
||||
throw new Error("globalThis.TextEncoder is not available, polyfill required");
|
||||
if (!globalThis.TextDecoder)
|
||||
throw new Error("globalThis.TextDecoder is not available, polyfill required");
|
||||
const g = new TextEncoder("utf-8"),
|
||||
y = new TextDecoder("utf-8");
|
||||
globalThis.Go = class {
|
||||
constructor() {
|
||||
(this.argv = ["js"]),
|
||||
(this.env = {}),
|
||||
(this.exit = (t) => {
|
||||
t !== 0 && console.warn("exit code:", t);
|
||||
}),
|
||||
(this._exitPromise = new Promise((t) => {
|
||||
this._resolveExitPromise = t;
|
||||
})),
|
||||
(this._pendingEvent = null),
|
||||
(this._scheduledTimeouts = new Map()),
|
||||
(this._nextCallbackTimeoutID = 1);
|
||||
const h = (t, e) => {
|
||||
this.mem.setUint32(t + 0, e, !0),
|
||||
this.mem.setUint32(t + 4, Math.floor(e / 4294967296), !0);
|
||||
},
|
||||
n = (t, e) => {
|
||||
this.mem.setUint32(t + 0, e, !0);
|
||||
},
|
||||
s = (t) => {
|
||||
const e = this.mem.getUint32(t + 0, !0),
|
||||
l = this.mem.getInt32(t + 4, !0);
|
||||
return e + l * 4294967296;
|
||||
},
|
||||
i = (t) => {
|
||||
const e = this.mem.getFloat64(t, !0);
|
||||
if (e === 0) return;
|
||||
if (!isNaN(e)) return e;
|
||||
const l = this.mem.getUint32(t, !0);
|
||||
return this._values[l];
|
||||
},
|
||||
r = (t, e) => {
|
||||
if (typeof e == "number" && e !== 0) {
|
||||
if (isNaN(e)) {
|
||||
this.mem.setUint32(t + 4, 2146959360, !0), this.mem.setUint32(t, 0, !0);
|
||||
return;
|
||||
}
|
||||
this.mem.setFloat64(t, e, !0);
|
||||
return;
|
||||
}
|
||||
if (e === void 0) {
|
||||
this.mem.setFloat64(t, 0, !0);
|
||||
return;
|
||||
}
|
||||
let a = this._ids.get(e);
|
||||
a === void 0 &&
|
||||
((a = this._idPool.pop()),
|
||||
a === void 0 && (a = this._values.length),
|
||||
(this._values[a] = e),
|
||||
(this._goRefCounts[a] = 0),
|
||||
this._ids.set(e, a)),
|
||||
this._goRefCounts[a]++;
|
||||
let c = 0;
|
||||
switch (typeof e) {
|
||||
case "object":
|
||||
e !== null && (c = 1);
|
||||
break;
|
||||
case "string":
|
||||
c = 2;
|
||||
break;
|
||||
case "symbol":
|
||||
c = 3;
|
||||
break;
|
||||
case "function":
|
||||
c = 4;
|
||||
break;
|
||||
}
|
||||
this.mem.setUint32(t + 4, 2146959360 | c, !0), this.mem.setUint32(t, a, !0);
|
||||
},
|
||||
f = (t) => {
|
||||
const e = s(t + 0),
|
||||
l = s(t + 8);
|
||||
return new Uint8Array(this._inst.exports.mem.buffer, e, l);
|
||||
},
|
||||
u = (t) => {
|
||||
const e = s(t + 0),
|
||||
l = s(t + 8),
|
||||
a = new Array(l);
|
||||
for (let c = 0; c < l; c++) a[c] = i(e + c * 8);
|
||||
return a;
|
||||
},
|
||||
d = (t) => {
|
||||
const e = s(t + 0),
|
||||
l = s(t + 8);
|
||||
return y.decode(new DataView(this._inst.exports.mem.buffer, e, l));
|
||||
},
|
||||
m = Date.now() - performance.now();
|
||||
this.importObject = {
|
||||
_gotest: { add: (t, e) => t + e },
|
||||
gojs: {
|
||||
"runtime.wasmExit": (t) => {
|
||||
t >>>= 0;
|
||||
const e = this.mem.getInt32(t + 8, !0);
|
||||
(this.exited = !0),
|
||||
delete this._inst,
|
||||
delete this._values,
|
||||
delete this._goRefCounts,
|
||||
delete this._ids,
|
||||
delete this._idPool,
|
||||
this.exit(e);
|
||||
},
|
||||
"runtime.wasmWrite": (t) => {
|
||||
t >>>= 0;
|
||||
const e = s(t + 8),
|
||||
l = s(t + 16),
|
||||
a = this.mem.getInt32(t + 24, !0);
|
||||
fs.writeSync(e, new Uint8Array(this._inst.exports.mem.buffer, l, a));
|
||||
},
|
||||
"runtime.resetMemoryDataView": (t) => {
|
||||
(t >>>= 0), (this.mem = new DataView(this._inst.exports.mem.buffer));
|
||||
},
|
||||
"runtime.nanotime1": (t) => {
|
||||
(t >>>= 0), h(t + 8, (m + performance.now()) * 1e6);
|
||||
},
|
||||
"runtime.walltime": (t) => {
|
||||
t >>>= 0;
|
||||
const e = new Date().getTime();
|
||||
h(t + 8, e / 1e3), this.mem.setInt32(t + 16, (e % 1e3) * 1e6, !0);
|
||||
},
|
||||
"runtime.scheduleTimeoutEvent": (t) => {
|
||||
t >>>= 0;
|
||||
const e = this._nextCallbackTimeoutID;
|
||||
this._nextCallbackTimeoutID++,
|
||||
this._scheduledTimeouts.set(
|
||||
e,
|
||||
setTimeout(() => {
|
||||
for (this._resume(); this._scheduledTimeouts.has(e); )
|
||||
console.warn("scheduleTimeoutEvent: missed timeout event"),
|
||||
this._resume();
|
||||
}, s(t + 8))
|
||||
),
|
||||
this.mem.setInt32(t + 16, e, !0);
|
||||
},
|
||||
"runtime.clearTimeoutEvent": (t) => {
|
||||
t >>>= 0;
|
||||
const e = this.mem.getInt32(t + 8, !0);
|
||||
clearTimeout(this._scheduledTimeouts.get(e)),
|
||||
this._scheduledTimeouts.delete(e);
|
||||
},
|
||||
"runtime.getRandomData": (t) => {
|
||||
(t >>>= 0), crypto.getRandomValues(f(t + 8));
|
||||
},
|
||||
"syscall/js.finalizeRef": (t) => {
|
||||
t >>>= 0;
|
||||
const e = this.mem.getUint32(t + 8, !0);
|
||||
if ((this._goRefCounts[e]--, this._goRefCounts[e] === 0)) {
|
||||
const l = this._values[e];
|
||||
(this._values[e] = null), this._ids.delete(l), this._idPool.push(e);
|
||||
}
|
||||
},
|
||||
"syscall/js.stringVal": (t) => {
|
||||
(t >>>= 0), r(t + 24, d(t + 8));
|
||||
},
|
||||
"syscall/js.valueGet": (t) => {
|
||||
t >>>= 0;
|
||||
const e = Reflect.get(i(t + 8), d(t + 16));
|
||||
(t = this._inst.exports.getsp() >>> 0), r(t + 32, e);
|
||||
},
|
||||
"syscall/js.valueSet": (t) => {
|
||||
(t >>>= 0), Reflect.set(i(t + 8), d(t + 16), i(t + 32));
|
||||
},
|
||||
"syscall/js.valueDelete": (t) => {
|
||||
(t >>>= 0), Reflect.deleteProperty(i(t + 8), d(t + 16));
|
||||
},
|
||||
"syscall/js.valueIndex": (t) => {
|
||||
(t >>>= 0), r(t + 24, Reflect.get(i(t + 8), s(t + 16)));
|
||||
},
|
||||
"syscall/js.valueSetIndex": (t) => {
|
||||
(t >>>= 0), Reflect.set(i(t + 8), s(t + 16), i(t + 24));
|
||||
},
|
||||
"syscall/js.valueCall": (t) => {
|
||||
t >>>= 0;
|
||||
try {
|
||||
const e = i(t + 8),
|
||||
l = Reflect.get(e, d(t + 16)),
|
||||
a = u(t + 32),
|
||||
c = Reflect.apply(l, e, a);
|
||||
(t = this._inst.exports.getsp() >>> 0),
|
||||
r(t + 56, c),
|
||||
this.mem.setUint8(t + 64, 1);
|
||||
} catch (e) {
|
||||
(t = this._inst.exports.getsp() >>> 0),
|
||||
r(t + 56, e),
|
||||
this.mem.setUint8(t + 64, 0);
|
||||
}
|
||||
},
|
||||
"syscall/js.valueInvoke": (t) => {
|
||||
t >>>= 0;
|
||||
try {
|
||||
const e = i(t + 8),
|
||||
l = u(t + 16),
|
||||
a = Reflect.apply(e, void 0, l);
|
||||
(t = this._inst.exports.getsp() >>> 0),
|
||||
r(t + 40, a),
|
||||
this.mem.setUint8(t + 48, 1);
|
||||
} catch (e) {
|
||||
(t = this._inst.exports.getsp() >>> 0),
|
||||
r(t + 40, e),
|
||||
this.mem.setUint8(t + 48, 0);
|
||||
}
|
||||
},
|
||||
"syscall/js.valueNew": (t) => {
|
||||
t >>>= 0;
|
||||
try {
|
||||
const e = i(t + 8),
|
||||
l = u(t + 16),
|
||||
a = Reflect.construct(e, l);
|
||||
(t = this._inst.exports.getsp() >>> 0),
|
||||
r(t + 40, a),
|
||||
this.mem.setUint8(t + 48, 1);
|
||||
} catch (e) {
|
||||
(t = this._inst.exports.getsp() >>> 0),
|
||||
r(t + 40, e),
|
||||
this.mem.setUint8(t + 48, 0);
|
||||
}
|
||||
},
|
||||
"syscall/js.valueLength": (t) => {
|
||||
(t >>>= 0), h(t + 16, parseInt(i(t + 8).length));
|
||||
},
|
||||
"syscall/js.valuePrepareString": (t) => {
|
||||
t >>>= 0;
|
||||
const e = g.encode(String(i(t + 8)));
|
||||
r(t + 16, e), h(t + 24, e.length);
|
||||
},
|
||||
"syscall/js.valueLoadString": (t) => {
|
||||
t >>>= 0;
|
||||
const e = i(t + 8);
|
||||
f(t + 16).set(e);
|
||||
},
|
||||
"syscall/js.valueInstanceOf": (t) => {
|
||||
(t >>>= 0), this.mem.setUint8(t + 24, i(t + 8) instanceof i(t + 16) ? 1 : 0);
|
||||
},
|
||||
"syscall/js.copyBytesToGo": (t) => {
|
||||
t >>>= 0;
|
||||
const e = f(t + 8),
|
||||
l = i(t + 32);
|
||||
if (!(l instanceof Uint8Array || l instanceof Uint8ClampedArray)) {
|
||||
this.mem.setUint8(t + 48, 0);
|
||||
return;
|
||||
}
|
||||
const a = l.subarray(0, e.length);
|
||||
e.set(a), h(t + 40, a.length), this.mem.setUint8(t + 48, 1);
|
||||
},
|
||||
"syscall/js.copyBytesToJS": (t) => {
|
||||
t >>>= 0;
|
||||
const e = i(t + 8),
|
||||
l = f(t + 16);
|
||||
if (!(e instanceof Uint8Array || e instanceof Uint8ClampedArray)) {
|
||||
this.mem.setUint8(t + 48, 0);
|
||||
return;
|
||||
}
|
||||
const a = l.subarray(0, e.length);
|
||||
e.set(a), h(t + 40, a.length), this.mem.setUint8(t + 48, 1);
|
||||
},
|
||||
debug: (t) => {
|
||||
console.log(t);
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
async run(h) {
|
||||
if (!(h instanceof WebAssembly.Instance))
|
||||
throw new Error("Go.run: WebAssembly.Instance expected");
|
||||
(this._inst = h),
|
||||
(this.mem = new DataView(this._inst.exports.mem.buffer)),
|
||||
(this._values = [NaN, 0, null, !0, !1, globalThis, this]),
|
||||
(this._goRefCounts = new Array(this._values.length).fill(1 / 0)),
|
||||
(this._ids = new Map([
|
||||
[0, 1],
|
||||
[null, 2],
|
||||
[!0, 3],
|
||||
[!1, 4],
|
||||
[globalThis, 5],
|
||||
[this, 6],
|
||||
])),
|
||||
(this._idPool = []),
|
||||
(this.exited = !1);
|
||||
let n = 4096;
|
||||
const s = (m) => {
|
||||
const t = n,
|
||||
e = g.encode(m + "\0");
|
||||
return (
|
||||
new Uint8Array(this.mem.buffer, n, e.length).set(e),
|
||||
(n += e.length),
|
||||
n % 8 !== 0 && (n += 8 - (n % 8)),
|
||||
t
|
||||
);
|
||||
},
|
||||
i = this.argv.length,
|
||||
r = [];
|
||||
this.argv.forEach((m) => {
|
||||
r.push(s(m));
|
||||
}),
|
||||
r.push(0),
|
||||
Object.keys(this.env)
|
||||
.sort()
|
||||
.forEach((m) => {
|
||||
r.push(s(`${m}=${this.env[m]}`));
|
||||
}),
|
||||
r.push(0);
|
||||
const u = n;
|
||||
if (
|
||||
(r.forEach((m) => {
|
||||
this.mem.setUint32(n, m, !0), this.mem.setUint32(n + 4, 0, !0), (n += 8);
|
||||
}),
|
||||
n >= 12288)
|
||||
)
|
||||
throw new Error(
|
||||
"total length of command line and environment variables exceeds limit"
|
||||
);
|
||||
this._inst.exports.run(i, u),
|
||||
this.exited && this._resolveExitPromise(),
|
||||
await this._exitPromise;
|
||||
}
|
||||
_resume() {
|
||||
if (this.exited) throw new Error("Go program has already exited");
|
||||
this._inst.exports.resume(), this.exited && this._resolveExitPromise();
|
||||
}
|
||||
_makeFuncWrapper(h) {
|
||||
const n = this;
|
||||
return function () {
|
||||
const s = { id: h, this: this, args: arguments };
|
||||
return (n._pendingEvent = s), n._resume(), s.result;
|
||||
};
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
|
@ -9,8 +9,7 @@ import (
|
|||
"sort"
|
||||
"strings"
|
||||
|
||||
"cdr.dev/slog"
|
||||
"github.com/dop251/goja"
|
||||
"log/slog"
|
||||
|
||||
"oss.terrastruct.com/util-go/xdefer"
|
||||
|
||||
|
|
@ -19,6 +18,7 @@ import (
|
|||
"oss.terrastruct.com/d2/d2graph"
|
||||
"oss.terrastruct.com/d2/d2target"
|
||||
"oss.terrastruct.com/d2/lib/geo"
|
||||
"oss.terrastruct.com/d2/lib/jsrunner"
|
||||
"oss.terrastruct.com/d2/lib/label"
|
||||
"oss.terrastruct.com/d2/lib/log"
|
||||
"oss.terrastruct.com/d2/lib/shape"
|
||||
|
|
@ -79,11 +79,11 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
|
|||
defer xdefer.Errorf(&err, "failed to dagre layout")
|
||||
|
||||
debugJS := false
|
||||
vm := goja.New()
|
||||
if _, err := vm.RunString(dagreJS); err != nil {
|
||||
runner := jsrunner.NewJSRunner()
|
||||
if _, err := runner.RunString(dagreJS); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := vm.RunString(setupJS); err != nil {
|
||||
if _, err := runner.RunString(setupJS); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
@ -135,7 +135,7 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
|
|||
}
|
||||
|
||||
configJS := setGraphAttrs(rootAttrs)
|
||||
if _, err := vm.RunString(configJS); err != nil {
|
||||
if _, err := runner.RunString(configJS); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
@ -179,22 +179,22 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
|
|||
}
|
||||
|
||||
if debugJS {
|
||||
log.Debug(ctx, "script", slog.F("all", setupJS+configJS+loadScript))
|
||||
log.Debug(ctx, "script", slog.Any("all", setupJS+configJS+loadScript))
|
||||
}
|
||||
|
||||
if _, err := vm.RunString(loadScript); err != nil {
|
||||
if _, err := runner.RunString(loadScript); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := vm.RunString(`dagre.layout(g)`); err != nil {
|
||||
if _, err := runner.RunString(`dagre.layout(g)`); err != nil {
|
||||
if debugJS {
|
||||
log.Warn(ctx, "layout error", slog.F("err", err))
|
||||
log.Warn(ctx, "layout error", slog.Any("err", err))
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
for i := range g.Objects {
|
||||
val, err := vm.RunString(fmt.Sprintf("JSON.stringify(g.node(g.nodes()[%d]))", i))
|
||||
val, err := runner.RunString(fmt.Sprintf("JSON.stringify(g.node(g.nodes()[%d]))", i))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -203,7 +203,7 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
|
|||
return err
|
||||
}
|
||||
if debugJS {
|
||||
log.Debug(ctx, "graph", slog.F("json", dn))
|
||||
log.Debug(ctx, "graph", slog.Any("json", dn))
|
||||
}
|
||||
|
||||
obj := mapper.ToObj(dn.ID)
|
||||
|
|
@ -215,7 +215,7 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
|
|||
}
|
||||
|
||||
for i, edge := range g.Edges {
|
||||
val, err := vm.RunString(fmt.Sprintf("JSON.stringify(g.edge(g.edges()[%d]))", i))
|
||||
val, err := runner.RunString(fmt.Sprintf("JSON.stringify(g.edge(g.edges()[%d]))", i))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -224,7 +224,7 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
|
|||
return err
|
||||
}
|
||||
if debugJS {
|
||||
log.Debug(ctx, "graph", slog.F("json", de))
|
||||
log.Debug(ctx, "graph", slog.Any("json", de))
|
||||
}
|
||||
|
||||
points := make([]*geo.Point, len(de.Points))
|
||||
|
|
@ -570,13 +570,12 @@ func positionLabelsIcons(obj *d2graph.Object) {
|
|||
} else {
|
||||
obj.LabelPosition = go2.Pointer(label.InsideMiddleCenter.String())
|
||||
}
|
||||
}
|
||||
|
||||
if float64(obj.LabelDimensions.Width) > obj.Width || float64(obj.LabelDimensions.Height) > obj.Height {
|
||||
if len(obj.ChildrenArray) > 0 {
|
||||
obj.LabelPosition = go2.Pointer(label.OutsideTopCenter.String())
|
||||
} else {
|
||||
obj.LabelPosition = go2.Pointer(label.OutsideBottomCenter.String())
|
||||
if float64(obj.LabelDimensions.Width) > obj.Width || float64(obj.LabelDimensions.Height) > obj.Height {
|
||||
if len(obj.ChildrenArray) > 0 {
|
||||
obj.LabelPosition = go2.Pointer(label.OutsideTopCenter.String())
|
||||
} else {
|
||||
obj.LabelPosition = go2.Pointer(label.OutsideBottomCenter.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
10
d2layouts/d2elklayout/elk.go
Normal file
10
d2layouts/d2elklayout/elk.go
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
//go:build !js && !wasm
|
||||
|
||||
package d2elklayout
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
)
|
||||
|
||||
//go:embed elk.js
|
||||
var elkJS string
|
||||
33
d2layouts/d2elklayout/elk.js
vendored
33
d2layouts/d2elklayout/elk.js
vendored
|
|
@ -5,15 +5,7 @@
|
|||
define([], f);
|
||||
} else {
|
||||
var g;
|
||||
if (typeof window !== "undefined") {
|
||||
g = window;
|
||||
} else if (typeof global !== "undefined") {
|
||||
g = global;
|
||||
} else if (typeof self !== "undefined") {
|
||||
g = self;
|
||||
} else {
|
||||
g = this;
|
||||
}
|
||||
g = this;
|
||||
g.ELK = f();
|
||||
}
|
||||
})(function () {
|
||||
|
|
@ -337,9 +329,6 @@
|
|||
|
||||
// -------------- FAKE ELEMENTS GWT ASSUMES EXIST --------------
|
||||
var $wnd = { Error: {} };
|
||||
if (typeof window !== "undefined") $wnd = window;
|
||||
else if (typeof global !== "undefined") $wnd = global; // nodejs
|
||||
else if (typeof self !== "undefined") $wnd = self; // web worker
|
||||
|
||||
var $moduleName, $moduleBase;
|
||||
|
||||
|
|
@ -59795,13 +59784,8 @@
|
|||
}, 0);
|
||||
};
|
||||
}
|
||||
if (typeof document === uke && typeof self !== uke) {
|
||||
var i = new h(self);
|
||||
self.onmessage = i.saveDispatch;
|
||||
} else if (typeof module !== uke && module.exports) {
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
module.exports = { default: j, Worker: j };
|
||||
}
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
module.exports = { default: j, Worker: j };
|
||||
}
|
||||
function aae(a) {
|
||||
if (a.N) return;
|
||||
|
|
@ -105682,16 +105666,7 @@
|
|||
// -------------- RUN GWT INITIALIZATION CODE --------------
|
||||
gwtOnLoad(null, "elk", null);
|
||||
}.call(this));
|
||||
}.call(
|
||||
this,
|
||||
typeof global !== "undefined"
|
||||
? global
|
||||
: typeof self !== "undefined"
|
||||
? self
|
||||
: typeof window !== "undefined"
|
||||
? window
|
||||
: {}
|
||||
));
|
||||
}.call(this, {}));
|
||||
},
|
||||
{},
|
||||
],
|
||||
|
|
|
|||
6
d2layouts/d2elklayout/elk_js.go
Normal file
6
d2layouts/d2elklayout/elk_js.go
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
//go:build js && wasm
|
||||
|
||||
package d2elklayout
|
||||
|
||||
// No embed, since this is already bundled in the js worker
|
||||
var elkJS string
|
||||
|
|
@ -8,15 +8,12 @@ import (
|
|||
"context"
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
|
||||
"oss.terrastruct.com/util-go/xdefer"
|
||||
|
||||
"oss.terrastruct.com/util-go/go2"
|
||||
|
|
@ -24,13 +21,11 @@ import (
|
|||
"oss.terrastruct.com/d2/d2graph"
|
||||
"oss.terrastruct.com/d2/d2target"
|
||||
"oss.terrastruct.com/d2/lib/geo"
|
||||
"oss.terrastruct.com/d2/lib/jsrunner"
|
||||
"oss.terrastruct.com/d2/lib/label"
|
||||
"oss.terrastruct.com/d2/lib/shape"
|
||||
)
|
||||
|
||||
//go:embed elk.js
|
||||
var elkJS string
|
||||
|
||||
//go:embed setup.js
|
||||
var setupJS string
|
||||
|
||||
|
|
@ -162,18 +157,20 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
|
|||
}
|
||||
defer xdefer.Errorf(&err, "failed to ELK layout")
|
||||
|
||||
vm := goja.New()
|
||||
runner := jsrunner.NewJSRunner()
|
||||
|
||||
console := vm.NewObject()
|
||||
if err := vm.Set("console", console); err != nil {
|
||||
return err
|
||||
}
|
||||
if runner.Engine() == jsrunner.Goja {
|
||||
console := runner.NewObject()
|
||||
if err := runner.Set("console", console); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := vm.RunString(elkJS); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := vm.RunString(setupJS); err != nil {
|
||||
return err
|
||||
if _, err := runner.RunString(elkJS); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := runner.RunString(setupJS); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
elkGraph := &ELKGraph{
|
||||
|
|
@ -443,41 +440,30 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
|
|||
return err
|
||||
}
|
||||
|
||||
loadScript := fmt.Sprintf(`var graph = %s`, raw)
|
||||
var val jsrunner.JSValue
|
||||
if runner.Engine() == jsrunner.Goja {
|
||||
loadScript := fmt.Sprintf(`var graph = %s`, raw)
|
||||
|
||||
if _, err := vm.RunString(loadScript); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := runner.RunString(loadScript); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
val, err := vm.RunString(`elk.layout(graph)
|
||||
val, err = runner.RunString(`elk.layout(graph)
|
||||
.then(s => s)
|
||||
.catch(err => err.message)
|
||||
`)
|
||||
|
||||
} else {
|
||||
val, err = runner.MustGet("elkResult")
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p := val.Export()
|
||||
result, err := runner.WaitPromise(ctx, val)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("ELK layout error: %v", err)
|
||||
}
|
||||
|
||||
promise := p.(*goja.Promise)
|
||||
|
||||
for promise.State() == goja.PromiseStatePending {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if promise.State() == goja.PromiseStateRejected {
|
||||
return errors.New("ELK: something went wrong")
|
||||
}
|
||||
|
||||
result := promise.Result().Export()
|
||||
|
||||
var jsonOut map[string]interface{}
|
||||
switch out := result.(type) {
|
||||
case string:
|
||||
|
|
@ -1148,13 +1134,12 @@ func positionLabelsIcons(obj *d2graph.Object) {
|
|||
} else {
|
||||
obj.LabelPosition = go2.Pointer(label.InsideMiddleCenter.String())
|
||||
}
|
||||
}
|
||||
|
||||
if float64(obj.LabelDimensions.Width) > obj.Width || float64(obj.LabelDimensions.Height) > obj.Height {
|
||||
if len(obj.ChildrenArray) > 0 {
|
||||
obj.LabelPosition = go2.Pointer(label.OutsideTopCenter.String())
|
||||
} else {
|
||||
obj.LabelPosition = go2.Pointer(label.OutsideBottomCenter.String())
|
||||
if float64(obj.LabelDimensions.Width) > obj.Width || float64(obj.LabelDimensions.Height) > obj.Height {
|
||||
if len(obj.ChildrenArray) > 0 {
|
||||
obj.LabelPosition = go2.Pointer(label.OutsideTopCenter.String())
|
||||
} else {
|
||||
obj.LabelPosition = go2.Pointer(label.OutsideBottomCenter.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
286
d2layouts/d2elklayout/wasm.go
Normal file
286
d2layouts/d2elklayout/wasm.go
Normal file
|
|
@ -0,0 +1,286 @@
|
|||
//go:build js && wasm
|
||||
|
||||
package d2elklayout
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
|
||||
"oss.terrastruct.com/d2/d2graph"
|
||||
"oss.terrastruct.com/d2/lib/geo"
|
||||
"oss.terrastruct.com/d2/lib/label"
|
||||
"oss.terrastruct.com/util-go/go2"
|
||||
"oss.terrastruct.com/util-go/xdefer"
|
||||
)
|
||||
|
||||
// This is mostly copy paste from Layout until elk.layout step
|
||||
func ConvertGraph(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (_ *ELKGraph, err error) {
|
||||
if opts == nil {
|
||||
opts = &DefaultOpts
|
||||
}
|
||||
defer xdefer.Errorf(&err, "failed to ELK layout")
|
||||
|
||||
elkGraph := &ELKGraph{
|
||||
ID: "",
|
||||
LayoutOptions: &elkOpts{
|
||||
Thoroughness: 8,
|
||||
EdgeEdgeBetweenLayersSpacing: 50,
|
||||
EdgeNode: edge_node_spacing,
|
||||
HierarchyHandling: "INCLUDE_CHILDREN",
|
||||
FixedAlignment: "BALANCED",
|
||||
ConsiderModelOrder: "NODES_AND_EDGES",
|
||||
CycleBreakingStrategy: "GREEDY_MODEL_ORDER",
|
||||
NodeSizeConstraints: "MINIMUM_SIZE",
|
||||
ContentAlignment: "H_CENTER V_CENTER",
|
||||
ConfigurableOpts: ConfigurableOpts{
|
||||
Algorithm: opts.Algorithm,
|
||||
NodeSpacing: opts.NodeSpacing,
|
||||
EdgeNodeSpacing: opts.EdgeNodeSpacing,
|
||||
SelfLoopSpacing: opts.SelfLoopSpacing,
|
||||
},
|
||||
},
|
||||
}
|
||||
if elkGraph.LayoutOptions.ConfigurableOpts.SelfLoopSpacing == DefaultOpts.SelfLoopSpacing {
|
||||
// +5 for a tiny bit of padding
|
||||
elkGraph.LayoutOptions.ConfigurableOpts.SelfLoopSpacing = go2.Max(elkGraph.LayoutOptions.ConfigurableOpts.SelfLoopSpacing, childrenMaxSelfLoop(g.Root, g.Root.Direction.Value == "down" || g.Root.Direction.Value == "" || g.Root.Direction.Value == "up")/2+5)
|
||||
}
|
||||
switch g.Root.Direction.Value {
|
||||
case "down":
|
||||
elkGraph.LayoutOptions.Direction = Down
|
||||
case "up":
|
||||
elkGraph.LayoutOptions.Direction = Up
|
||||
case "right":
|
||||
elkGraph.LayoutOptions.Direction = Right
|
||||
case "left":
|
||||
elkGraph.LayoutOptions.Direction = Left
|
||||
default:
|
||||
elkGraph.LayoutOptions.Direction = Down
|
||||
}
|
||||
|
||||
// set label and icon positions for ELK
|
||||
for _, obj := range g.Objects {
|
||||
positionLabelsIcons(obj)
|
||||
}
|
||||
|
||||
adjustments := make(map[*d2graph.Object]geo.Spacing)
|
||||
elkNodes := make(map[*d2graph.Object]*ELKNode)
|
||||
elkEdges := make(map[*d2graph.Edge]*ELKEdge)
|
||||
|
||||
// BFS
|
||||
var walk func(*d2graph.Object, *d2graph.Object, func(*d2graph.Object, *d2graph.Object))
|
||||
walk = func(obj, parent *d2graph.Object, fn func(*d2graph.Object, *d2graph.Object)) {
|
||||
if obj.Parent != nil {
|
||||
fn(obj, parent)
|
||||
}
|
||||
for _, ch := range obj.ChildrenArray {
|
||||
walk(ch, obj, fn)
|
||||
}
|
||||
}
|
||||
|
||||
walk(g.Root, nil, func(obj, parent *d2graph.Object) {
|
||||
incoming := 0.
|
||||
outgoing := 0.
|
||||
for _, e := range g.Edges {
|
||||
if e.Src == obj {
|
||||
outgoing++
|
||||
}
|
||||
if e.Dst == obj {
|
||||
incoming++
|
||||
}
|
||||
}
|
||||
if incoming >= 2 || outgoing >= 2 {
|
||||
switch g.Root.Direction.Value {
|
||||
case "right", "left":
|
||||
if obj.Attributes.HeightAttr == nil {
|
||||
obj.Height = math.Max(obj.Height, math.Max(incoming, outgoing)*port_spacing)
|
||||
}
|
||||
default:
|
||||
if obj.Attributes.WidthAttr == nil {
|
||||
obj.Width = math.Max(obj.Width, math.Max(incoming, outgoing)*port_spacing)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if obj.HasLabel() && obj.HasIcon() {
|
||||
// this gives shapes extra height for their label if they also have an icon
|
||||
obj.Height += float64(obj.LabelDimensions.Height + label.PADDING)
|
||||
}
|
||||
|
||||
margin, _ := obj.SpacingOpt(label.PADDING, label.PADDING, false)
|
||||
width := margin.Left + obj.Width + margin.Right
|
||||
height := margin.Top + obj.Height + margin.Bottom
|
||||
adjustments[obj] = margin
|
||||
|
||||
n := &ELKNode{
|
||||
ID: obj.AbsID(),
|
||||
Width: width,
|
||||
Height: height,
|
||||
}
|
||||
|
||||
if len(obj.ChildrenArray) > 0 {
|
||||
n.LayoutOptions = &elkOpts{
|
||||
ForceNodeModelOrder: true,
|
||||
Thoroughness: 8,
|
||||
EdgeEdgeBetweenLayersSpacing: 50,
|
||||
HierarchyHandling: "INCLUDE_CHILDREN",
|
||||
FixedAlignment: "BALANCED",
|
||||
EdgeNode: edge_node_spacing,
|
||||
ConsiderModelOrder: "NODES_AND_EDGES",
|
||||
CycleBreakingStrategy: "GREEDY_MODEL_ORDER",
|
||||
NodeSizeConstraints: "MINIMUM_SIZE",
|
||||
ContentAlignment: "H_CENTER V_CENTER",
|
||||
ConfigurableOpts: ConfigurableOpts{
|
||||
NodeSpacing: opts.NodeSpacing,
|
||||
EdgeNodeSpacing: opts.EdgeNodeSpacing,
|
||||
SelfLoopSpacing: opts.SelfLoopSpacing,
|
||||
Padding: opts.Padding,
|
||||
},
|
||||
}
|
||||
if n.LayoutOptions.ConfigurableOpts.SelfLoopSpacing == DefaultOpts.SelfLoopSpacing {
|
||||
n.LayoutOptions.ConfigurableOpts.SelfLoopSpacing = go2.Max(n.LayoutOptions.ConfigurableOpts.SelfLoopSpacing, childrenMaxSelfLoop(obj, g.Root.Direction.Value == "down" || g.Root.Direction.Value == "" || g.Root.Direction.Value == "up")/2+5)
|
||||
}
|
||||
|
||||
switch elkGraph.LayoutOptions.Direction {
|
||||
case Down, Up:
|
||||
n.LayoutOptions.NodeSizeMinimum = fmt.Sprintf("(%d, %d)", int(math.Ceil(height)), int(math.Ceil(width)))
|
||||
case Right, Left:
|
||||
n.LayoutOptions.NodeSizeMinimum = fmt.Sprintf("(%d, %d)", int(math.Ceil(width)), int(math.Ceil(height)))
|
||||
}
|
||||
} else {
|
||||
n.LayoutOptions = &elkOpts{
|
||||
SelfLoopDistribution: "EQUALLY",
|
||||
}
|
||||
}
|
||||
|
||||
if obj.IsContainer() {
|
||||
padding := parsePadding(opts.Padding)
|
||||
padding = adjustPadding(obj, width, height, padding)
|
||||
n.LayoutOptions.Padding = padding.String()
|
||||
}
|
||||
|
||||
if obj.HasLabel() {
|
||||
n.Labels = append(n.Labels, &ELKLabel{
|
||||
Text: obj.Label.Value,
|
||||
Width: float64(obj.LabelDimensions.Width),
|
||||
Height: float64(obj.LabelDimensions.Height),
|
||||
})
|
||||
}
|
||||
|
||||
if parent == g.Root {
|
||||
elkGraph.Children = append(elkGraph.Children, n)
|
||||
} else {
|
||||
elkNodes[parent].Children = append(elkNodes[parent].Children, n)
|
||||
}
|
||||
|
||||
if obj.SQLTable != nil {
|
||||
n.LayoutOptions.PortConstraints = "FIXED_POS"
|
||||
columns := obj.SQLTable.Columns
|
||||
colHeight := n.Height / float64(len(columns)+1)
|
||||
n.Ports = make([]*ELKPort, 0, len(columns)*2)
|
||||
var srcSide, dstSide PortSide
|
||||
switch elkGraph.LayoutOptions.Direction {
|
||||
case Left:
|
||||
srcSide, dstSide = West, East
|
||||
default:
|
||||
srcSide, dstSide = East, West
|
||||
}
|
||||
for i, col := range columns {
|
||||
n.Ports = append(n.Ports, &ELKPort{
|
||||
ID: srcPortID(obj, col.Name.Label),
|
||||
Y: float64(i+1)*colHeight + colHeight/2,
|
||||
LayoutOptions: &elkOpts{PortSide: srcSide},
|
||||
})
|
||||
n.Ports = append(n.Ports, &ELKPort{
|
||||
ID: dstPortID(obj, col.Name.Label),
|
||||
Y: float64(i+1)*colHeight + colHeight/2,
|
||||
LayoutOptions: &elkOpts{PortSide: dstSide},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
elkNodes[obj] = n
|
||||
})
|
||||
|
||||
var srcSide, dstSide PortSide
|
||||
switch elkGraph.LayoutOptions.Direction {
|
||||
case Up:
|
||||
srcSide, dstSide = North, South
|
||||
default:
|
||||
srcSide, dstSide = South, North
|
||||
}
|
||||
|
||||
ports := map[struct {
|
||||
obj *d2graph.Object
|
||||
side PortSide
|
||||
}][]*ELKPort{}
|
||||
|
||||
for ei, edge := range g.Edges {
|
||||
var src, dst string
|
||||
|
||||
switch {
|
||||
case edge.SrcTableColumnIndex != nil:
|
||||
src = srcPortID(edge.Src, edge.Src.SQLTable.Columns[*edge.SrcTableColumnIndex].Name.Label)
|
||||
case edge.Src.SQLTable != nil:
|
||||
p := &ELKPort{
|
||||
ID: fmt.Sprintf("%s.%d", srcPortID(edge.Src, "__root__"), ei),
|
||||
LayoutOptions: &elkOpts{PortSide: srcSide},
|
||||
}
|
||||
src = p.ID
|
||||
elkNodes[edge.Src].Ports = append(elkNodes[edge.Src].Ports, p)
|
||||
k := struct {
|
||||
obj *d2graph.Object
|
||||
side PortSide
|
||||
}{edge.Src, srcSide}
|
||||
ports[k] = append(ports[k], p)
|
||||
default:
|
||||
src = edge.Src.AbsID()
|
||||
}
|
||||
|
||||
switch {
|
||||
case edge.DstTableColumnIndex != nil:
|
||||
dst = dstPortID(edge.Dst, edge.Dst.SQLTable.Columns[*edge.DstTableColumnIndex].Name.Label)
|
||||
case edge.Dst.SQLTable != nil:
|
||||
p := &ELKPort{
|
||||
ID: fmt.Sprintf("%s.%d", dstPortID(edge.Dst, "__root__"), ei),
|
||||
LayoutOptions: &elkOpts{PortSide: dstSide},
|
||||
}
|
||||
dst = p.ID
|
||||
elkNodes[edge.Dst].Ports = append(elkNodes[edge.Dst].Ports, p)
|
||||
k := struct {
|
||||
obj *d2graph.Object
|
||||
side PortSide
|
||||
}{edge.Dst, dstSide}
|
||||
ports[k] = append(ports[k], p)
|
||||
default:
|
||||
dst = edge.Dst.AbsID()
|
||||
}
|
||||
|
||||
e := &ELKEdge{
|
||||
ID: edge.AbsID(),
|
||||
Sources: []string{src},
|
||||
Targets: []string{dst},
|
||||
}
|
||||
if edge.Label.Value != "" {
|
||||
e.Labels = append(e.Labels, &ELKLabel{
|
||||
Text: edge.Label.Value,
|
||||
Width: float64(edge.LabelDimensions.Width),
|
||||
Height: float64(edge.LabelDimensions.Height),
|
||||
LayoutOptions: &elkOpts{
|
||||
InlineEdgeLabels: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
elkGraph.Edges = append(elkGraph.Edges, e)
|
||||
elkEdges[edge] = e
|
||||
}
|
||||
|
||||
for k, ports := range ports {
|
||||
width := elkNodes[k.obj].Width
|
||||
spacing := width / float64(len(ports)+1)
|
||||
for i, p := range ports {
|
||||
p.X = float64(i+1) * spacing
|
||||
}
|
||||
}
|
||||
return elkGraph, nil
|
||||
}
|
||||
|
|
@ -3,12 +3,11 @@ package d2layouts
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"math"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"cdr.dev/slog"
|
||||
|
||||
"oss.terrastruct.com/d2/d2graph"
|
||||
"oss.terrastruct.com/d2/d2layouts/d2grid"
|
||||
"oss.terrastruct.com/d2/d2layouts/d2near"
|
||||
|
|
@ -203,7 +202,7 @@ func LayoutNested(ctx context.Context, g *d2graph.Graph, graphInfo GraphInfo, co
|
|||
extractedEdges = append(extractedEdges, externalEdges...)
|
||||
extractedEdgeIDs = append(extractedEdgeIDs, externalEdgeIDs...)
|
||||
|
||||
log.Info(ctx, "layout nested", slog.F("level", curr.Level()), slog.F("child", curr.AbsID()), slog.F("gi", gi))
|
||||
log.Debug(ctx, "layout nested", slog.Any("level", curr.Level()), slog.Any("child", curr.AbsID()), slog.Any("gi", gi))
|
||||
nestedInfo := gi
|
||||
nearKey := curr.NearKey
|
||||
if gi.IsConstantNear {
|
||||
|
|
@ -250,19 +249,19 @@ func LayoutNested(ctx context.Context, g *d2graph.Graph, graphInfo GraphInfo, co
|
|||
if len(g.Objects) > 0 {
|
||||
switch graphInfo.DiagramType {
|
||||
case GridDiagram:
|
||||
log.Debug(ctx, "layout grid", slog.F("rootlevel", g.RootLevel), slog.F("shapes", g.PrintString()))
|
||||
log.Debug(ctx, "layout grid", slog.Any("rootlevel", g.RootLevel), slog.Any("shapes", g.PrintString()))
|
||||
if err = d2grid.Layout(ctx, g); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case SequenceDiagram:
|
||||
log.Debug(ctx, "layout sequence", slog.F("rootlevel", g.RootLevel), slog.F("shapes", g.PrintString()))
|
||||
log.Debug(ctx, "layout sequence", slog.Any("rootlevel", g.RootLevel), slog.Any("shapes", g.PrintString()))
|
||||
err = d2sequence.Layout(ctx, g, coreLayout)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
log.Debug(ctx, "default layout", slog.F("rootlevel", g.RootLevel), slog.F("shapes", g.PrintString()))
|
||||
log.Debug(ctx, "default layout", slog.Any("rootlevel", g.RootLevel), slog.Any("shapes", g.PrintString()))
|
||||
err := coreLayout(ctx, g)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -337,7 +336,7 @@ func LayoutNested(ctx context.Context, g *d2graph.Graph, graphInfo GraphInfo, co
|
|||
}
|
||||
}
|
||||
|
||||
log.Debug(ctx, "done", slog.F("rootlevel", g.RootLevel), slog.F("shapes", g.PrintString()))
|
||||
log.Debug(ctx, "done", slog.Any("rootlevel", g.RootLevel), slog.Any("shapes", g.PrintString()))
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -190,6 +190,20 @@ func boundingBox(g *d2graph.Graph) (tl, br *geo.Point) {
|
|||
}
|
||||
}
|
||||
|
||||
for _, edge := range g.Edges {
|
||||
if edge.Src.OuterNearContainer() != nil || edge.Dst.OuterNearContainer() != nil {
|
||||
continue
|
||||
}
|
||||
if edge.Route != nil {
|
||||
for _, point := range edge.Route {
|
||||
x1 = math.Min(x1, point.X)
|
||||
y1 = math.Min(y1, point.Y)
|
||||
x2 = math.Max(x2, point.X)
|
||||
y2 = math.Max(y2, point.Y)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if math.IsInf(x1, 1) && math.IsInf(x2, -1) {
|
||||
x1 = 0
|
||||
x2 = 0
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package d2sequence
|
|||
|
||||
// units of space on the left/right when computing the space required between actors
|
||||
const HORIZONTAL_PAD = 40.
|
||||
const LABEL_HORIZONTAL_PAD = 60.
|
||||
|
||||
// units of space on the top/bottom when computing the space required between messages
|
||||
// TODO lower
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"oss.terrastruct.com/d2/d2ast"
|
||||
"oss.terrastruct.com/d2/d2compiler"
|
||||
"oss.terrastruct.com/d2/d2graph"
|
||||
"oss.terrastruct.com/d2/d2layouts/d2sequence"
|
||||
|
|
@ -50,7 +51,7 @@ n2 -> n1
|
|||
|
||||
nEdges := len(g.Edges)
|
||||
|
||||
ctx := log.WithTB(context.Background(), t, nil)
|
||||
ctx := log.WithTB(context.Background(), t)
|
||||
d2sequence.Layout(ctx, g, func(ctx context.Context, g *d2graph.Graph) error {
|
||||
// just set some position as if it had been properly placed
|
||||
for _, obj := range g.Objects {
|
||||
|
|
@ -176,7 +177,7 @@ b.t1 -> a.t1
|
|||
a.t2 -> b
|
||||
b -> a.t2`
|
||||
|
||||
ctx := log.WithTB(context.Background(), t, nil)
|
||||
ctx := log.WithTB(context.Background(), t)
|
||||
g, _, err := d2compiler.Compile("", strings.NewReader(input), nil)
|
||||
assert.Nil(t, err)
|
||||
|
||||
|
|
@ -297,7 +298,7 @@ func TestNestedSequenceDiagrams(t *testing.T) {
|
|||
c
|
||||
container -> c: edge 1
|
||||
`
|
||||
ctx := log.WithTB(context.Background(), t, nil)
|
||||
ctx := log.WithTB(context.Background(), t)
|
||||
g, _, err := d2compiler.Compile("", strings.NewReader(input), nil)
|
||||
assert.Nil(t, err)
|
||||
|
||||
|
|
@ -321,7 +322,7 @@ container -> c: edge 1
|
|||
assert.True(t, has)
|
||||
b_t1.Box = geo.NewBox(nil, 100, 100)
|
||||
|
||||
c := g.Root.EnsureChild([]string{"c"})
|
||||
c := g.Root.EnsureChild([]d2ast.String{d2ast.FlatUnquotedString("c")})
|
||||
c.Box = geo.NewBox(nil, 100, 100)
|
||||
c.Shape = d2graph.Scalar{Value: d2target.ShapeSquare}
|
||||
|
||||
|
|
@ -379,7 +380,7 @@ container -> c: edge 1
|
|||
func TestSelfEdges(t *testing.T) {
|
||||
g := d2graph.NewGraph()
|
||||
g.Root.Shape = d2graph.Scalar{Value: d2target.ShapeSequenceDiagram}
|
||||
n1 := g.Root.EnsureChild([]string{"n1"})
|
||||
n1 := g.Root.EnsureChild([]d2ast.String{d2ast.FlatUnquotedString("n1")})
|
||||
n1.Box = geo.NewBox(nil, 100, 100)
|
||||
|
||||
g.Edges = []*d2graph.Edge{
|
||||
|
|
@ -393,7 +394,7 @@ func TestSelfEdges(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
ctx := log.WithTB(context.Background(), t, nil)
|
||||
ctx := log.WithTB(context.Background(), t)
|
||||
d2sequence.Layout(ctx, g, func(ctx context.Context, g *d2graph.Graph) error {
|
||||
return nil
|
||||
})
|
||||
|
|
@ -415,12 +416,12 @@ func TestSelfEdges(t *testing.T) {
|
|||
func TestSequenceToDescendant(t *testing.T) {
|
||||
g := d2graph.NewGraph()
|
||||
g.Root.Shape = d2graph.Scalar{Value: d2target.ShapeSequenceDiagram}
|
||||
a := g.Root.EnsureChild([]string{"a"})
|
||||
a := g.Root.EnsureChild([]d2ast.String{d2ast.FlatUnquotedString("a")})
|
||||
a.Box = geo.NewBox(nil, 100, 100)
|
||||
a.Attributes = d2graph.Attributes{
|
||||
Shape: d2graph.Scalar{Value: shape.PERSON_TYPE},
|
||||
}
|
||||
a_t1 := a.EnsureChild([]string{"t1"})
|
||||
a_t1 := a.EnsureChild([]d2ast.String{d2ast.FlatUnquotedString("t1")})
|
||||
a_t1.Box = geo.NewBox(nil, 16, 80)
|
||||
|
||||
g.Edges = []*d2graph.Edge{
|
||||
|
|
@ -435,7 +436,7 @@ func TestSequenceToDescendant(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
ctx := log.WithTB(context.Background(), t, nil)
|
||||
ctx := log.WithTB(context.Background(), t)
|
||||
d2sequence.Layout(ctx, g, func(ctx context.Context, g *d2graph.Graph) error {
|
||||
return nil
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
package d2sequence
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
|
@ -50,6 +52,9 @@ func getObjEarliestLineNum(o *d2graph.Object) int {
|
|||
if ref.MapKey == nil {
|
||||
continue
|
||||
}
|
||||
if ref.Key.HasGlob() {
|
||||
continue
|
||||
}
|
||||
min = go2.IntMin(min, ref.MapKey.Range.Start.Line)
|
||||
}
|
||||
return min
|
||||
|
|
@ -61,6 +66,9 @@ func getEdgeEarliestLineNum(e *d2graph.Edge) int {
|
|||
if ref.MapKey == nil {
|
||||
continue
|
||||
}
|
||||
if ref.Edge.Src.HasGlob() || ref.Edge.Dst.HasGlob() {
|
||||
continue
|
||||
}
|
||||
min = go2.IntMin(min, ref.MapKey.Range.Start.Line)
|
||||
}
|
||||
return min
|
||||
|
|
@ -70,6 +78,13 @@ func newSequenceDiagram(objects []*d2graph.Object, messages []*d2graph.Edge) (*s
|
|||
var actors []*d2graph.Object
|
||||
var groups []*d2graph.Object
|
||||
|
||||
slices.SortFunc(objects, func(a, b *d2graph.Object) int {
|
||||
return cmp.Compare(getObjEarliestLineNum(a), getObjEarliestLineNum(b))
|
||||
})
|
||||
slices.SortFunc(messages, func(a, b *d2graph.Edge) int {
|
||||
return cmp.Compare(getEdgeEarliestLineNum(a), getEdgeEarliestLineNum(b))
|
||||
})
|
||||
|
||||
for _, obj := range objects {
|
||||
if obj.IsSequenceDiagramGroup() {
|
||||
queue := []*d2graph.Object{obj}
|
||||
|
|
@ -162,17 +177,21 @@ func newSequenceDiagram(objects []*d2graph.Object, messages []*d2graph.Edge) (*s
|
|||
|
||||
for _, message := range sd.messages {
|
||||
sd.verticalIndices[message.AbsID()] = getEdgeEarliestLineNum(message)
|
||||
// TODO this should not be global yStep, only affect the neighbors
|
||||
sd.yStep = math.Max(sd.yStep, float64(message.LabelDimensions.Height))
|
||||
|
||||
// ensures that long labels, spanning over multiple actors, don't make for large gaps between actors
|
||||
// by distributing the label length across the actors rank difference
|
||||
rankDiff := math.Abs(float64(sd.objectRank[message.Src]) - float64(sd.objectRank[message.Dst]))
|
||||
if rankDiff != 0 {
|
||||
// rankDiff = 0 for self edges
|
||||
distributedLabelWidth := float64(message.LabelDimensions.Width) / rankDiff
|
||||
for rank := go2.IntMin(sd.objectRank[message.Src], sd.objectRank[message.Dst]); rank <= go2.IntMax(sd.objectRank[message.Src], sd.objectRank[message.Dst])-1; rank++ {
|
||||
sd.actorXStep[rank] = math.Max(sd.actorXStep[rank], distributedLabelWidth+HORIZONTAL_PAD)
|
||||
sd.actorXStep[rank] = math.Max(sd.actorXStep[rank], distributedLabelWidth+LABEL_HORIZONTAL_PAD)
|
||||
}
|
||||
} else {
|
||||
// self edge
|
||||
nextRank := sd.objectRank[message.Src]
|
||||
if nextRank < len(sd.actorXStep) {
|
||||
labelAdjust := float64(message.LabelDimensions.Width) + label.PADDING*4
|
||||
sd.actorXStep[nextRank] = math.Max(sd.actorXStep[nextRank], labelAdjust)
|
||||
}
|
||||
}
|
||||
sd.lastMessage[message.Src] = message
|
||||
|
|
@ -229,10 +248,12 @@ func (sd *sequenceDiagram) placeGroup(group *d2graph.Object) {
|
|||
for _, m := range sd.messages {
|
||||
if m.ContainedBy(group) {
|
||||
for _, p := range m.Route {
|
||||
labelHeight := float64(m.LabelDimensions.Height) / 2.
|
||||
edgePad := math.Max(labelHeight+GROUP_CONTAINER_PADDING, MIN_MESSAGE_DISTANCE/2.)
|
||||
minX = math.Min(minX, p.X-HORIZONTAL_PAD)
|
||||
minY = math.Min(minY, p.Y-MIN_MESSAGE_DISTANCE/2.)
|
||||
minY = math.Min(minY, p.Y-edgePad)
|
||||
maxX = math.Max(maxX, p.X+HORIZONTAL_PAD)
|
||||
maxY = math.Max(maxY, p.Y+MIN_MESSAGE_DISTANCE/2.)
|
||||
maxY = math.Max(maxY, p.Y+edgePad)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -240,6 +261,9 @@ func (sd *sequenceDiagram) placeGroup(group *d2graph.Object) {
|
|||
for _, n := range sd.notes {
|
||||
inGroup := false
|
||||
for _, ref := range n.References {
|
||||
if ref.Key.HasGlob() {
|
||||
continue
|
||||
}
|
||||
curr := ref.ScopeObj
|
||||
for curr != nil {
|
||||
if curr == group {
|
||||
|
|
@ -287,8 +311,8 @@ func (sd *sequenceDiagram) adjustGroupLabel(group *d2graph.Object) {
|
|||
return
|
||||
}
|
||||
|
||||
heightAdd := (group.LabelDimensions.Height + EDGE_GROUP_LABEL_PADDING) - GROUP_CONTAINER_PADDING
|
||||
if heightAdd < 0 {
|
||||
heightAdd := (group.LabelDimensions.Height + EDGE_GROUP_LABEL_PADDING/2.)
|
||||
if heightAdd < GROUP_CONTAINER_PADDING {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -329,7 +353,6 @@ func (sd *sequenceDiagram) adjustGroupLabel(group *d2graph.Object) {
|
|||
n.TopLeft.Y += float64(heightAdd)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// placeActors places actors bottom aligned, side by side with centers spaced by sd.actorXStep
|
||||
|
|
@ -443,7 +466,12 @@ func (sd *sequenceDiagram) placeNotes() {
|
|||
|
||||
for _, msg := range sd.messages {
|
||||
if sd.verticalIndices[msg.AbsID()] < verticalIndex {
|
||||
y += sd.yStep
|
||||
if msg.Src == msg.Dst {
|
||||
// For self-messages, account for the full vertical space they occupy
|
||||
y += sd.yStep + math.Max(float64(msg.LabelDimensions.Height), MIN_MESSAGE_DISTANCE)*1.5
|
||||
} else {
|
||||
y += sd.yStep + float64(msg.LabelDimensions.Height)
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, otherNote := range sd.notes {
|
||||
|
|
@ -536,8 +564,6 @@ func (sd *sequenceDiagram) placeSpans() {
|
|||
// routeMessages routes horizontal edges (messages) from Src to Dst lifeline (actor/span center)
|
||||
// in another step, routes are adjusted to spans borders when necessary
|
||||
func (sd *sequenceDiagram) routeMessages() error {
|
||||
var prevIsLoop bool
|
||||
var prevGroup *d2graph.Object
|
||||
messageOffset := sd.maxActorHeight + sd.yStep
|
||||
for _, message := range sd.messages {
|
||||
message.ZIndex = MESSAGE_Z_INDEX
|
||||
|
|
@ -548,15 +574,6 @@ func (sd *sequenceDiagram) routeMessages() error {
|
|||
}
|
||||
}
|
||||
|
||||
// we need extra space if the previous message is a loop in a different group
|
||||
group := message.GetGroup()
|
||||
if prevIsLoop && prevGroup != group {
|
||||
messageOffset += MIN_MESSAGE_DISTANCE
|
||||
}
|
||||
prevGroup = group
|
||||
|
||||
startY := messageOffset + noteOffset
|
||||
|
||||
var startX, endX float64
|
||||
if startCenter := getCenter(message.Src); startCenter != nil {
|
||||
startX = startCenter.X
|
||||
|
|
@ -583,23 +600,24 @@ func (sd *sequenceDiagram) routeMessages() error {
|
|||
isToSibling := currSrc == currDst
|
||||
|
||||
if isSelfMessage || isToDescendant || isFromDescendant || isToSibling {
|
||||
midX := startX + SELF_MESSAGE_HORIZONTAL_TRAVEL
|
||||
endY := startY + MIN_MESSAGE_DISTANCE*1.5
|
||||
midX := startX + math.Max(SELF_MESSAGE_HORIZONTAL_TRAVEL, float64(message.LabelDimensions.Width)/2.+label.PADDING*2)
|
||||
startY := messageOffset + noteOffset
|
||||
endY := startY + math.Max(float64(message.LabelDimensions.Height), MIN_MESSAGE_DISTANCE)*1.5
|
||||
message.Route = []*geo.Point{
|
||||
geo.NewPoint(startX, startY),
|
||||
geo.NewPoint(midX, startY),
|
||||
geo.NewPoint(midX, endY),
|
||||
geo.NewPoint(endX, endY),
|
||||
}
|
||||
prevIsLoop = true
|
||||
messageOffset = endY + sd.yStep - noteOffset
|
||||
} else {
|
||||
startY := messageOffset + noteOffset + float64(message.LabelDimensions.Height/2.)
|
||||
message.Route = []*geo.Point{
|
||||
geo.NewPoint(startX, startY),
|
||||
geo.NewPoint(endX, startY),
|
||||
}
|
||||
prevIsLoop = false
|
||||
messageOffset = startY + float64(message.LabelDimensions.Height/2.) + sd.yStep - noteOffset
|
||||
}
|
||||
messageOffset += sd.yStep
|
||||
|
||||
if message.Label.Value != "" {
|
||||
message.LabelPosition = go2.Pointer(label.InsideMiddleCenter.String())
|
||||
|
|
@ -647,7 +665,19 @@ func (sd *sequenceDiagram) isActor(obj *d2graph.Object) bool {
|
|||
func (sd *sequenceDiagram) getWidth() float64 {
|
||||
// the layout is always placed starting at 0, so the width is just the last actor
|
||||
lastActor := sd.actors[len(sd.actors)-1]
|
||||
return lastActor.TopLeft.X + lastActor.Width
|
||||
rightmost := lastActor.TopLeft.X + lastActor.Width
|
||||
|
||||
for _, m := range sd.messages {
|
||||
for _, p := range m.Route {
|
||||
rightmost = math.Max(rightmost, p.X)
|
||||
}
|
||||
// Self referential messages may have labels that extend further
|
||||
if m.Src == m.Dst {
|
||||
rightmost = math.Max(rightmost, m.Route[1].X+float64(m.LabelDimensions.Width)/2.)
|
||||
}
|
||||
}
|
||||
|
||||
return rightmost
|
||||
}
|
||||
|
||||
func (sd *sequenceDiagram) getHeight() float64 {
|
||||
|
|
|
|||
12
d2lib/d2.go
12
d2lib/d2.go
|
|
@ -71,9 +71,21 @@ func Compile(ctx context.Context, input string, compileOpts *CompileOptions, ren
|
|||
|
||||
applyConfigs(config, compileOpts, renderOpts)
|
||||
applyDefaults(compileOpts, renderOpts)
|
||||
if config != nil {
|
||||
g.Data = config.Data
|
||||
}
|
||||
|
||||
d, err := compile(ctx, g, compileOpts, renderOpts)
|
||||
if d != nil {
|
||||
if config == nil {
|
||||
config = &d2target.Config{}
|
||||
}
|
||||
// These are fields that affect a diagram's appearance, so feed them back
|
||||
// into diagram.Config to ensure the hash computed for CSS styling purposes
|
||||
// is unique to its appearance
|
||||
config.ThemeID = renderOpts.ThemeID
|
||||
config.DarkThemeID = renderOpts.DarkThemeID
|
||||
config.Sketch = renderOpts.Sketch
|
||||
d.Config = config
|
||||
}
|
||||
return d, g, err
|
||||
|
|
|
|||
506
d2lsp/completion.go
Normal file
506
d2lsp/completion.go
Normal file
|
|
@ -0,0 +1,506 @@
|
|||
// Completion implements lsp autocomplete features
|
||||
// Currently handles:
|
||||
// - Complete dot and inside maps for reserved keyword holders (style, labels, etc)
|
||||
// - Complete discrete values for keywords like shape
|
||||
// - Complete suggestions for formats for keywords like opacity
|
||||
package d2lsp
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"oss.terrastruct.com/d2/d2ast"
|
||||
"oss.terrastruct.com/d2/d2parser"
|
||||
"oss.terrastruct.com/d2/d2target"
|
||||
)
|
||||
|
||||
type CompletionKind int
|
||||
|
||||
const (
|
||||
KeywordCompletion CompletionKind = iota
|
||||
StyleCompletion
|
||||
ShapeCompletion
|
||||
)
|
||||
|
||||
type CompletionItem struct {
|
||||
Label string
|
||||
Kind CompletionKind
|
||||
Detail string
|
||||
InsertText string
|
||||
}
|
||||
|
||||
func GetCompletionItems(text string, line, column int) ([]CompletionItem, error) {
|
||||
ast, err := d2parser.Parse("", strings.NewReader(text), nil)
|
||||
if err != nil {
|
||||
ast, _ = d2parser.Parse("", strings.NewReader(getTextUntilPosition(text, line, column)), nil)
|
||||
}
|
||||
|
||||
keyword := getKeywordContext(text, ast, line, column)
|
||||
switch keyword {
|
||||
case "style", "style.":
|
||||
return getStyleCompletions(), nil
|
||||
case "shape", "shape:":
|
||||
return getShapeCompletions(), nil
|
||||
case "shadow", "3d", "multiple", "animated", "bold", "italic", "underline", "filled", "double-border",
|
||||
"shadow:", "3d:", "multiple:", "animated:", "bold:", "italic:", "underline:", "filled:", "double-border:",
|
||||
"style.shadow:", "style.3d:", "style.multiple:", "style.animated:", "style.bold:", "style.italic:", "style.underline:", "style.filled:", "style.double-border:":
|
||||
return getBooleanCompletions(), nil
|
||||
case "fill-pattern", "fill-pattern:", "style.fill-pattern:":
|
||||
return getFillPatternCompletions(), nil
|
||||
case "text-transform", "text-transform:", "style.text-transform:":
|
||||
return getTextTransformCompletions(), nil
|
||||
case "opacity", "stroke-width", "stroke-dash", "border-radius", "font-size",
|
||||
"stroke", "fill", "font-color":
|
||||
return getValueCompletions(keyword), nil
|
||||
case "opacity:", "stroke-width:", "stroke-dash:", "border-radius:", "font-size:",
|
||||
"stroke:", "fill:", "font-color:",
|
||||
"style.opacity:", "style.stroke-width:", "style.stroke-dash:", "style.border-radius:", "style.font-size:",
|
||||
"style.stroke:", "style.fill:", "style.font-color:":
|
||||
return getValueCompletions(strings.TrimSuffix(strings.TrimPrefix(keyword, "style."), ":")), nil
|
||||
case "width", "height", "top", "left":
|
||||
return getValueCompletions(keyword), nil
|
||||
case "width:", "height:", "top:", "left:":
|
||||
return getValueCompletions(keyword[:len(keyword)-1]), nil
|
||||
case "source-arrowhead", "target-arrowhead":
|
||||
return getArrowheadCompletions(), nil
|
||||
case "source-arrowhead.shape:", "target-arrowhead.shape:":
|
||||
return getArrowheadShapeCompletions(), nil
|
||||
case "label", "label.":
|
||||
return getLabelCompletions(), nil
|
||||
case "icon", "icon:":
|
||||
return getIconCompletions(), nil
|
||||
case "icon.":
|
||||
return getLabelCompletions(), nil
|
||||
case "near", "near:":
|
||||
return getNearCompletions(), nil
|
||||
case "tooltip:", "tooltip":
|
||||
return getTooltipCompletions(), nil
|
||||
case "direction:", "direction":
|
||||
return getDirectionCompletions(), nil
|
||||
default:
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
func getTextUntilPosition(text string, line, column int) string {
|
||||
lines := strings.Split(text, "\n")
|
||||
if line >= len(lines) {
|
||||
return text
|
||||
}
|
||||
|
||||
result := strings.Join(lines[:line], "\n")
|
||||
if len(result) > 0 {
|
||||
result += "\n"
|
||||
}
|
||||
if column > len(lines[line]) {
|
||||
result += lines[line]
|
||||
} else {
|
||||
result += lines[line][:column]
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func getKeywordContext(text string, m *d2ast.Map, line, column int) string {
|
||||
if m == nil {
|
||||
return ""
|
||||
}
|
||||
lines := strings.Split(text, "\n")
|
||||
|
||||
for _, n := range m.Nodes {
|
||||
if n.MapKey == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var firstPart, lastPart string
|
||||
var key *d2ast.KeyPath
|
||||
if len(n.MapKey.Edges) > 0 {
|
||||
key = n.MapKey.EdgeKey
|
||||
} else {
|
||||
key = n.MapKey.Key
|
||||
}
|
||||
if key != nil && len(key.Path) > 0 {
|
||||
firstKey := key.Path[0].Unbox()
|
||||
if !firstKey.IsUnquoted() {
|
||||
continue
|
||||
}
|
||||
firstPart = firstKey.ScalarString()
|
||||
|
||||
pathLen := len(key.Path)
|
||||
if pathLen > 1 {
|
||||
lastKey := key.Path[pathLen-1].Unbox()
|
||||
if lastKey.IsUnquoted() {
|
||||
lastPart = lastKey.ScalarString()
|
||||
_, isHolderLast := d2ast.ReservedKeywordHolders[lastPart]
|
||||
if !isHolderLast {
|
||||
_, isHolderLast = d2ast.CompositeReservedKeywords[lastPart]
|
||||
}
|
||||
keyRange := n.MapKey.Range
|
||||
lineText := lines[keyRange.End.Line]
|
||||
if isHolderLast && isAfterDot(lineText, column) {
|
||||
return lastPart + "."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if _, isBoard := d2ast.BoardKeywords[firstPart]; isBoard {
|
||||
firstPart = ""
|
||||
}
|
||||
if firstPart == "classes" {
|
||||
firstPart = ""
|
||||
}
|
||||
|
||||
_, isHolder := d2ast.ReservedKeywordHolders[firstPart]
|
||||
if !isHolder {
|
||||
_, isHolder = d2ast.CompositeReservedKeywords[firstPart]
|
||||
}
|
||||
|
||||
// Check nested map
|
||||
if n.MapKey.Value.Map != nil && isPositionInMap(line, column, n.MapKey.Value.Map) {
|
||||
if nested := getKeywordContext(text, n.MapKey.Value.Map, line, column); nested != "" {
|
||||
if isHolder {
|
||||
// If we got a direct key completion from inside a holder's map,
|
||||
// prefix it with the holder's name
|
||||
if strings.HasSuffix(nested, ":") && !strings.Contains(nested, ".") {
|
||||
return firstPart + "." + strings.TrimSuffix(nested, ":") + ":"
|
||||
}
|
||||
}
|
||||
return nested
|
||||
}
|
||||
return firstPart
|
||||
}
|
||||
|
||||
keyRange := n.MapKey.Range
|
||||
if line != keyRange.End.Line {
|
||||
continue
|
||||
}
|
||||
|
||||
// 1) Skip if cursor is well above/below this key
|
||||
if line < keyRange.Start.Line || line > keyRange.End.Line {
|
||||
continue
|
||||
}
|
||||
|
||||
// 2) If on the start line, skip if before the key
|
||||
if line == keyRange.Start.Line && column < keyRange.Start.Column {
|
||||
continue
|
||||
}
|
||||
|
||||
// 3) If on the end line, allow up to keyRange.End.Column + 1
|
||||
if line == keyRange.End.Line && column > keyRange.End.Column+1 {
|
||||
continue
|
||||
}
|
||||
|
||||
lineText := lines[keyRange.End.Line]
|
||||
|
||||
if isAfterColon(lineText, column) {
|
||||
if key != nil && len(key.Path) > 1 {
|
||||
if isHolder && (firstPart == "source-arrowhead" || firstPart == "target-arrowhead") {
|
||||
return firstPart + "." + lastPart + ":"
|
||||
}
|
||||
|
||||
_, isHolder := d2ast.ReservedKeywordHolders[lastPart]
|
||||
if !isHolder {
|
||||
return lastPart
|
||||
}
|
||||
}
|
||||
return firstPart + ":"
|
||||
}
|
||||
|
||||
if isAfterDot(lineText, column) && isHolder {
|
||||
return firstPart
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func isAfterDot(text string, pos int) bool {
|
||||
return pos > 0 && pos <= len(text) && text[pos-1] == '.'
|
||||
}
|
||||
|
||||
func isAfterColon(text string, pos int) bool {
|
||||
if pos < 1 || pos > len(text) {
|
||||
return false
|
||||
}
|
||||
i := pos - 1
|
||||
for i >= 0 && unicode.IsSpace(rune(text[i])) {
|
||||
i--
|
||||
}
|
||||
return i >= 0 && text[i] == ':'
|
||||
}
|
||||
|
||||
func isPositionInMap(line, column int, m *d2ast.Map) bool {
|
||||
if m == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
mapRange := m.Range
|
||||
if line < mapRange.Start.Line || line > mapRange.End.Line {
|
||||
return false
|
||||
}
|
||||
|
||||
if line == mapRange.Start.Line && column < mapRange.Start.Column {
|
||||
return false
|
||||
}
|
||||
if line == mapRange.End.Line && column > mapRange.End.Column {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func getShapeCompletions() []CompletionItem {
|
||||
items := make([]CompletionItem, 0, len(d2target.Shapes))
|
||||
for _, shape := range d2target.Shapes {
|
||||
item := CompletionItem{
|
||||
Label: shape,
|
||||
Kind: ShapeCompletion,
|
||||
Detail: "shape",
|
||||
InsertText: shape,
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
func getValueCompletions(property string) []CompletionItem {
|
||||
switch property {
|
||||
case "opacity":
|
||||
return []CompletionItem{{
|
||||
Label: "(number between 0.0 and 1.0)",
|
||||
Kind: KeywordCompletion,
|
||||
Detail: "e.g. 0.4",
|
||||
InsertText: "",
|
||||
}}
|
||||
case "stroke-width":
|
||||
return []CompletionItem{{
|
||||
Label: "(number between 0 and 15)",
|
||||
Kind: KeywordCompletion,
|
||||
Detail: "e.g. 2",
|
||||
InsertText: "",
|
||||
}}
|
||||
case "font-size":
|
||||
return []CompletionItem{{
|
||||
Label: "(number between 8 and 100)",
|
||||
Kind: KeywordCompletion,
|
||||
Detail: "e.g. 14",
|
||||
InsertText: "",
|
||||
}}
|
||||
case "stroke-dash":
|
||||
return []CompletionItem{{
|
||||
Label: "(number between 0 and 10)",
|
||||
Kind: KeywordCompletion,
|
||||
Detail: "e.g. 5",
|
||||
InsertText: "",
|
||||
}}
|
||||
case "border-radius":
|
||||
return []CompletionItem{{
|
||||
Label: "(number greater than or equal to 0)",
|
||||
Kind: KeywordCompletion,
|
||||
Detail: "e.g. 4",
|
||||
InsertText: "",
|
||||
}}
|
||||
case "font-color", "stroke", "fill":
|
||||
return []CompletionItem{{
|
||||
Label: "(color name or hex code)",
|
||||
Kind: KeywordCompletion,
|
||||
Detail: "e.g. blue, #ff0000",
|
||||
InsertText: "",
|
||||
}}
|
||||
case "width", "height", "top", "left":
|
||||
return []CompletionItem{{
|
||||
Label: "(pixels)",
|
||||
Kind: KeywordCompletion,
|
||||
Detail: "e.g. 400",
|
||||
InsertText: "",
|
||||
}}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getStyleCompletions() []CompletionItem {
|
||||
items := make([]CompletionItem, 0, len(d2ast.StyleKeywords))
|
||||
for keyword := range d2ast.StyleKeywords {
|
||||
item := CompletionItem{
|
||||
Label: keyword,
|
||||
Kind: StyleCompletion,
|
||||
Detail: "style property",
|
||||
InsertText: keyword + ": ",
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
func getBooleanCompletions() []CompletionItem {
|
||||
return []CompletionItem{
|
||||
{
|
||||
Label: "true",
|
||||
Kind: KeywordCompletion,
|
||||
Detail: "boolean",
|
||||
InsertText: "true",
|
||||
},
|
||||
{
|
||||
Label: "false",
|
||||
Kind: KeywordCompletion,
|
||||
Detail: "boolean",
|
||||
InsertText: "false",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func getFillPatternCompletions() []CompletionItem {
|
||||
items := make([]CompletionItem, 0, len(d2ast.FillPatterns))
|
||||
for _, pattern := range d2ast.FillPatterns {
|
||||
item := CompletionItem{
|
||||
Label: pattern,
|
||||
Kind: KeywordCompletion,
|
||||
Detail: "fill pattern",
|
||||
InsertText: pattern,
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
func getTextTransformCompletions() []CompletionItem {
|
||||
items := make([]CompletionItem, 0, len(d2ast.TextTransforms))
|
||||
for _, transform := range d2ast.TextTransforms {
|
||||
item := CompletionItem{
|
||||
Label: transform,
|
||||
Kind: KeywordCompletion,
|
||||
Detail: "text transform",
|
||||
InsertText: transform,
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
func isOnEmptyLine(text string, line int) bool {
|
||||
lines := strings.Split(text, "\n")
|
||||
if line >= len(lines) {
|
||||
return true
|
||||
}
|
||||
|
||||
return strings.TrimSpace(lines[line]) == ""
|
||||
}
|
||||
|
||||
func getLabelCompletions() []CompletionItem {
|
||||
return []CompletionItem{{
|
||||
Label: "near",
|
||||
Kind: StyleCompletion,
|
||||
Detail: "label position",
|
||||
InsertText: "near: ",
|
||||
}}
|
||||
}
|
||||
|
||||
func getNearCompletions() []CompletionItem {
|
||||
items := make([]CompletionItem, 0, len(d2ast.LabelPositionsArray)+1)
|
||||
|
||||
items = append(items, CompletionItem{
|
||||
Label: "(object ID)",
|
||||
Kind: KeywordCompletion,
|
||||
Detail: "e.g. container.inner_shape",
|
||||
InsertText: "",
|
||||
})
|
||||
|
||||
for _, pos := range d2ast.LabelPositionsArray {
|
||||
item := CompletionItem{
|
||||
Label: pos,
|
||||
Kind: KeywordCompletion,
|
||||
Detail: "label position",
|
||||
InsertText: pos,
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
func getTooltipCompletions() []CompletionItem {
|
||||
return []CompletionItem{
|
||||
{
|
||||
Label: "(markdown)",
|
||||
Kind: KeywordCompletion,
|
||||
Detail: "markdown formatted text",
|
||||
InsertText: "|md\n # Tooltip\n Hello world\n|",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func getIconCompletions() []CompletionItem {
|
||||
return []CompletionItem{
|
||||
{
|
||||
Label: "(URL, e.g. https://icons.terrastruct.com/xyz.svg)",
|
||||
Kind: KeywordCompletion,
|
||||
Detail: "icon URL",
|
||||
InsertText: "https://icons.terrastruct.com/essentials%2F073-add.svg",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func getDirectionCompletions() []CompletionItem {
|
||||
directions := []string{"up", "down", "right", "left"}
|
||||
items := make([]CompletionItem, len(directions))
|
||||
for i, dir := range directions {
|
||||
items[i] = CompletionItem{
|
||||
Label: dir,
|
||||
Kind: KeywordCompletion,
|
||||
Detail: "direction",
|
||||
InsertText: dir,
|
||||
}
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
func getArrowheadShapeCompletions() []CompletionItem {
|
||||
arrowheads := []string{
|
||||
"triangle",
|
||||
"arrow",
|
||||
"diamond",
|
||||
"circle",
|
||||
"cf-one", "cf-one-required",
|
||||
"cf-many", "cf-many-required",
|
||||
}
|
||||
|
||||
items := make([]CompletionItem, len(arrowheads))
|
||||
details := map[string]string{
|
||||
"triangle": "default",
|
||||
"arrow": "like triangle but pointier",
|
||||
"cf-one": "crows foot one",
|
||||
"cf-one-required": "crows foot one (required)",
|
||||
"cf-many": "crows foot many",
|
||||
"cf-many-required": "crows foot many (required)",
|
||||
}
|
||||
|
||||
for i, shape := range arrowheads {
|
||||
detail := details[shape]
|
||||
if detail == "" {
|
||||
detail = "arrowhead shape"
|
||||
}
|
||||
items[i] = CompletionItem{
|
||||
Label: shape,
|
||||
Kind: ShapeCompletion,
|
||||
Detail: detail,
|
||||
InsertText: shape,
|
||||
}
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
func getArrowheadCompletions() []CompletionItem {
|
||||
completions := []string{
|
||||
"shape",
|
||||
"label",
|
||||
"style.filled",
|
||||
}
|
||||
|
||||
items := make([]CompletionItem, len(completions))
|
||||
|
||||
for i, shape := range completions {
|
||||
items[i] = CompletionItem{
|
||||
Label: shape,
|
||||
Kind: ShapeCompletion,
|
||||
InsertText: shape,
|
||||
}
|
||||
}
|
||||
return items
|
||||
}
|
||||
454
d2lsp/completion_test.go
Normal file
454
d2lsp/completion_test.go
Normal file
|
|
@ -0,0 +1,454 @@
|
|||
package d2lsp
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetCompletionItems(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
text string
|
||||
line int
|
||||
column int
|
||||
want []CompletionItem
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "style dot suggestions",
|
||||
text: "a.style.",
|
||||
line: 0,
|
||||
column: 8,
|
||||
want: getStyleCompletions(),
|
||||
},
|
||||
{
|
||||
name: "style map suggestions",
|
||||
text: `a: {
|
||||
style.
|
||||
}
|
||||
`,
|
||||
line: 1,
|
||||
column: 8,
|
||||
want: getStyleCompletions(),
|
||||
},
|
||||
{
|
||||
name: "classes shapes",
|
||||
text: `classes: {
|
||||
goal: {
|
||||
shape:
|
||||
}
|
||||
}
|
||||
`,
|
||||
line: 2,
|
||||
column: 10,
|
||||
want: getShapeCompletions(),
|
||||
},
|
||||
{
|
||||
name: "nested style map suggestions",
|
||||
text: `a: {
|
||||
style: {
|
||||
3d:
|
||||
}
|
||||
}
|
||||
`,
|
||||
line: 2,
|
||||
column: 7,
|
||||
want: getBooleanCompletions(),
|
||||
},
|
||||
{
|
||||
name: "3d style map suggestions",
|
||||
text: `a.style: {
|
||||
3d:
|
||||
}
|
||||
`,
|
||||
line: 1,
|
||||
column: 5,
|
||||
want: getBooleanCompletions(),
|
||||
},
|
||||
{
|
||||
name: "fill pattern style map suggestions",
|
||||
text: `a.style: {
|
||||
fill-pattern:
|
||||
}
|
||||
`,
|
||||
line: 1,
|
||||
column: 15,
|
||||
want: getFillPatternCompletions(),
|
||||
},
|
||||
{
|
||||
name: "opacity style map suggestions",
|
||||
text: `a.style: {
|
||||
opacity:
|
||||
}
|
||||
`,
|
||||
line: 1,
|
||||
column: 10,
|
||||
want: getValueCompletions("opacity"),
|
||||
},
|
||||
{
|
||||
name: "width dot",
|
||||
text: `a.width:`,
|
||||
line: 0,
|
||||
column: 8,
|
||||
want: getValueCompletions("width"),
|
||||
},
|
||||
{
|
||||
name: "layer shape",
|
||||
text: `a
|
||||
|
||||
layers: {
|
||||
hey: {
|
||||
go: {
|
||||
shape:
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
line: 5,
|
||||
column: 12,
|
||||
want: getShapeCompletions(),
|
||||
},
|
||||
{
|
||||
name: "stroke width value",
|
||||
text: `a.style.stroke-width: 1`,
|
||||
line: 0,
|
||||
column: 23,
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "no style suggestions",
|
||||
text: `a.style:
|
||||
`,
|
||||
line: 0,
|
||||
column: 8,
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "style property suggestions",
|
||||
text: "a -> b: { style. }",
|
||||
line: 0,
|
||||
column: 16,
|
||||
want: getStyleCompletions(),
|
||||
},
|
||||
{
|
||||
name: "style.opacity value hint",
|
||||
text: "a -> b: { style.opacity: }",
|
||||
line: 0,
|
||||
column: 24,
|
||||
want: getValueCompletions("opacity"),
|
||||
},
|
||||
{
|
||||
name: "fill pattern completions",
|
||||
text: "a -> b: { style.fill-pattern: }",
|
||||
line: 0,
|
||||
column: 29,
|
||||
want: getFillPatternCompletions(),
|
||||
},
|
||||
{
|
||||
name: "text transform completions",
|
||||
text: "a -> b: { style.text-transform: }",
|
||||
line: 0,
|
||||
column: 31,
|
||||
want: getTextTransformCompletions(),
|
||||
},
|
||||
{
|
||||
name: "boolean property completions",
|
||||
text: "a -> b: { style.shadow: }",
|
||||
line: 0,
|
||||
column: 23,
|
||||
want: getBooleanCompletions(),
|
||||
},
|
||||
{
|
||||
name: "near position completions",
|
||||
text: "a -> b: { label.near: }",
|
||||
line: 0,
|
||||
column: 21,
|
||||
want: getNearCompletions(),
|
||||
},
|
||||
{
|
||||
name: "direction completions",
|
||||
text: "a -> b: { direction: }",
|
||||
line: 0,
|
||||
column: 20,
|
||||
want: getDirectionCompletions(),
|
||||
},
|
||||
{
|
||||
name: "icon url completions",
|
||||
text: "a -> b: { icon: }",
|
||||
line: 0,
|
||||
column: 15,
|
||||
want: getIconCompletions(),
|
||||
},
|
||||
{
|
||||
name: "icon dot url completions",
|
||||
text: "a.icon:",
|
||||
line: 0,
|
||||
column: 7,
|
||||
want: getIconCompletions(),
|
||||
},
|
||||
{
|
||||
name: "icon near completions",
|
||||
text: "a -> b: { icon.near: }",
|
||||
line: 0,
|
||||
column: 20,
|
||||
want: getNearCompletions(),
|
||||
},
|
||||
{
|
||||
name: "icon map",
|
||||
text: `a.icon: {
|
||||
# here
|
||||
}`,
|
||||
line: 1,
|
||||
column: 2,
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "icon flat dot",
|
||||
text: `a.icon.`,
|
||||
line: 0,
|
||||
column: 7,
|
||||
want: getLabelCompletions(),
|
||||
},
|
||||
{
|
||||
name: "label flat dot",
|
||||
text: `a.label.`,
|
||||
line: 0,
|
||||
column: 8,
|
||||
want: getLabelCompletions(),
|
||||
},
|
||||
{
|
||||
name: "arrowhead completions - dot syntax",
|
||||
text: "a -> b: { source-arrowhead. }",
|
||||
line: 0,
|
||||
column: 27,
|
||||
want: getArrowheadCompletions(),
|
||||
},
|
||||
{
|
||||
name: "arrowhead completions - colon syntax",
|
||||
text: "a -> b: { source-arrowhead: }",
|
||||
line: 0,
|
||||
column: 27,
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "arrowhead completions - map syntax",
|
||||
text: `a -> b: {
|
||||
source-arrowhead: {
|
||||
# here
|
||||
}
|
||||
}`,
|
||||
line: 2,
|
||||
column: 4,
|
||||
want: getArrowheadCompletions(),
|
||||
},
|
||||
{
|
||||
name: "arrowhead shape completions - flat dot syntax",
|
||||
text: "(a -> b)[0].source-arrowhead.shape:",
|
||||
line: 0,
|
||||
column: 35,
|
||||
want: getArrowheadShapeCompletions(),
|
||||
},
|
||||
{
|
||||
name: "arrowhead shape completions - dot syntax",
|
||||
text: "a -> b: { source-arrowhead.shape: }",
|
||||
line: 0,
|
||||
column: 33,
|
||||
want: getArrowheadShapeCompletions(),
|
||||
},
|
||||
{
|
||||
name: "arrowhead shape completions - map syntax",
|
||||
text: "a -> b: { source-arrowhead: { shape: } }",
|
||||
line: 0,
|
||||
column: 36,
|
||||
want: getArrowheadShapeCompletions(),
|
||||
},
|
||||
{
|
||||
name: "width value hint",
|
||||
text: "a -> b: { width: }",
|
||||
line: 0,
|
||||
column: 16,
|
||||
want: getValueCompletions("width"),
|
||||
},
|
||||
{
|
||||
name: "height value hint",
|
||||
text: "a -> b: { height: }",
|
||||
line: 0,
|
||||
column: 17,
|
||||
want: getValueCompletions("height"),
|
||||
},
|
||||
{
|
||||
name: "tooltip markdown template",
|
||||
text: "a -> b: { tooltip: }",
|
||||
line: 0,
|
||||
column: 18,
|
||||
want: getTooltipCompletions(),
|
||||
},
|
||||
{
|
||||
name: "tooltip dot markdown template",
|
||||
text: "a.tooltip:",
|
||||
line: 0,
|
||||
column: 10,
|
||||
want: getTooltipCompletions(),
|
||||
},
|
||||
{
|
||||
name: "shape dot suggestions",
|
||||
text: "a.shape:",
|
||||
line: 0,
|
||||
column: 8,
|
||||
want: getShapeCompletions(),
|
||||
},
|
||||
{
|
||||
name: "shape suggestions",
|
||||
text: "a -> b: { shape: }",
|
||||
line: 0,
|
||||
column: 16,
|
||||
want: getShapeCompletions(),
|
||||
},
|
||||
{
|
||||
name: "shape 2 suggestions",
|
||||
text: `a: {
|
||||
shape:
|
||||
}`,
|
||||
line: 1,
|
||||
column: 8,
|
||||
want: getShapeCompletions(),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := GetCompletionItems(tt.text, tt.line, tt.column)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("GetCompletionItems() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
|
||||
if len(got) != len(tt.want) {
|
||||
t.Errorf("GetCompletionItems() got %d completions, want %d", len(got), len(tt.want))
|
||||
return
|
||||
}
|
||||
|
||||
// Create maps for easy comparison
|
||||
gotMap := make(map[string]CompletionItem)
|
||||
wantMap := make(map[string]CompletionItem)
|
||||
for _, item := range got {
|
||||
gotMap[item.Label] = item
|
||||
}
|
||||
for _, item := range tt.want {
|
||||
wantMap[item.Label] = item
|
||||
}
|
||||
|
||||
// Check that each completion exists and has correct properties
|
||||
for label, wantItem := range wantMap {
|
||||
gotItem, exists := gotMap[label]
|
||||
if !exists {
|
||||
t.Errorf("missing completion for %q", label)
|
||||
continue
|
||||
}
|
||||
if gotItem.Kind != wantItem.Kind {
|
||||
t.Errorf("completion %q Kind = %v, want %v", label, gotItem.Kind, wantItem.Kind)
|
||||
}
|
||||
if gotItem.Detail != wantItem.Detail {
|
||||
t.Errorf("completion %q Detail = %v, want %v", label, gotItem.Detail, wantItem.Detail)
|
||||
}
|
||||
if gotItem.InsertText != wantItem.InsertText {
|
||||
t.Errorf("completion %q InsertText = %v, want %v", label, gotItem.InsertText, wantItem.InsertText)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to compare CompletionItem slices
|
||||
func equalCompletions(a, b []CompletionItem) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i := range a {
|
||||
if a[i].Label != b[i].Label ||
|
||||
a[i].Kind != b[i].Kind ||
|
||||
a[i].Detail != b[i].Detail ||
|
||||
a[i].InsertText != b[i].InsertText {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func TestGetArrowheadShapeCompletions(t *testing.T) {
|
||||
got := getArrowheadShapeCompletions()
|
||||
|
||||
expectedLabels := []string{
|
||||
"triangle", "arrow", "diamond", "circle",
|
||||
"cf-one", "cf-one-required",
|
||||
"cf-many", "cf-many-required",
|
||||
}
|
||||
|
||||
if len(got) != len(expectedLabels) {
|
||||
t.Errorf("getArrowheadShapeCompletions() returned %d items, want %d", len(got), len(expectedLabels))
|
||||
return
|
||||
}
|
||||
|
||||
for i, label := range expectedLabels {
|
||||
if got[i].Label != label {
|
||||
t.Errorf("completion[%d].Label = %v, want %v", i, got[i].Label, label)
|
||||
}
|
||||
if got[i].Kind != ShapeCompletion {
|
||||
t.Errorf("completion[%d].Kind = %v, want ShapeCompletion", i, got[i].Kind)
|
||||
}
|
||||
if got[i].InsertText != label {
|
||||
t.Errorf("completion[%d].InsertText = %v, want %v", i, got[i].InsertText, label)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetValueCompletions(t *testing.T) {
|
||||
tests := []struct {
|
||||
property string
|
||||
wantLabel string
|
||||
wantDetail string
|
||||
}{
|
||||
{
|
||||
property: "opacity",
|
||||
wantLabel: "(number between 0.0 and 1.0)",
|
||||
wantDetail: "e.g. 0.4",
|
||||
},
|
||||
{
|
||||
property: "stroke-width",
|
||||
wantLabel: "(number between 0 and 15)",
|
||||
wantDetail: "e.g. 2",
|
||||
},
|
||||
{
|
||||
property: "font-size",
|
||||
wantLabel: "(number between 8 and 100)",
|
||||
wantDetail: "e.g. 14",
|
||||
},
|
||||
{
|
||||
property: "width",
|
||||
wantLabel: "(pixels)",
|
||||
wantDetail: "e.g. 400",
|
||||
},
|
||||
{
|
||||
property: "stroke",
|
||||
wantLabel: "(color name or hex code)",
|
||||
wantDetail: "e.g. blue, #ff0000",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.property, func(t *testing.T) {
|
||||
got := getValueCompletions(tt.property)
|
||||
if len(got) != 1 {
|
||||
t.Fatalf("getValueCompletions(%s) returned %d items, want 1", tt.property, len(got))
|
||||
}
|
||||
if got[0].Label != tt.wantLabel {
|
||||
t.Errorf("completion.Label = %v, want %v", got[0].Label, tt.wantLabel)
|
||||
}
|
||||
if got[0].Detail != tt.wantDetail {
|
||||
t.Errorf("completion.Detail = %v, want %v", got[0].Detail, tt.wantDetail)
|
||||
}
|
||||
if got[0].InsertText != "" {
|
||||
t.Errorf("completion.InsertText = %v, want empty string", got[0].InsertText)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
157
d2lsp/d2lsp.go
Normal file
157
d2lsp/d2lsp.go
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
// d2lsp contains functions useful for IDE clients
|
||||
package d2lsp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"oss.terrastruct.com/d2/d2ast"
|
||||
"oss.terrastruct.com/d2/d2ir"
|
||||
"oss.terrastruct.com/d2/d2parser"
|
||||
"oss.terrastruct.com/d2/lib/memfs"
|
||||
)
|
||||
|
||||
func GetRefRanges(path string, fs map[string]string, boardPath []string, key string) (ranges []d2ast.Range, importRanges []d2ast.Range, _ error) {
|
||||
m, err := getBoardMap(path, fs, boardPath)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
mk, err := d2parser.ParseMapKey(key)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if mk.Key == nil && len(mk.Edges) == 0 {
|
||||
return nil, nil, fmt.Errorf(`"%s" is invalid`, key)
|
||||
}
|
||||
|
||||
var f *d2ir.Field
|
||||
if mk.Key != nil {
|
||||
for _, p := range mk.Key.Path {
|
||||
f = m.GetField(p.Unbox())
|
||||
if f == nil {
|
||||
return nil, nil, nil
|
||||
}
|
||||
m = f.Map()
|
||||
}
|
||||
}
|
||||
|
||||
if len(mk.Edges) > 0 {
|
||||
eids := d2ir.NewEdgeIDs(mk)
|
||||
var edges []*d2ir.Edge
|
||||
for _, eid := range eids {
|
||||
edges = append(edges, m.GetEdges(eid, nil, nil)...)
|
||||
}
|
||||
if len(edges) == 0 {
|
||||
return nil, nil, nil
|
||||
}
|
||||
for _, edge := range edges {
|
||||
for _, ref := range edge.References {
|
||||
ranges = append(ranges, ref.AST().GetRange())
|
||||
}
|
||||
if edge.ImportAST() != nil {
|
||||
importRanges = append(importRanges, edge.ImportAST().GetRange())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, ref := range f.References {
|
||||
ranges = append(ranges, ref.AST().GetRange())
|
||||
}
|
||||
if f.ImportAST() != nil {
|
||||
importRanges = append(importRanges, f.ImportAST().GetRange())
|
||||
}
|
||||
}
|
||||
return ranges, importRanges, nil
|
||||
}
|
||||
|
||||
func getBoardMap(path string, fs map[string]string, boardPath []string) (*d2ir.Map, error) {
|
||||
if _, ok := fs[path]; !ok {
|
||||
return nil, fmt.Errorf(`"%s" not found`, path)
|
||||
}
|
||||
r := strings.NewReader(fs[path])
|
||||
ast, err := d2parser.Parse(path, r, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mfs, err := memfs.New(fs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m, _, err := d2ir.Compile(ast, &d2ir.CompileOptions{
|
||||
FS: mfs,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m = m.FindBoardRoot(boardPath)
|
||||
if m == nil {
|
||||
return nil, fmt.Errorf(`board "%v" not found`, boardPath)
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func GetBoardAtPosition(text string, pos d2ast.Position) ([]string, error) {
|
||||
r := strings.NewReader(text)
|
||||
ast, err := d2parser.Parse("", r, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pos.Byte = -1
|
||||
return getBoardPathAtPosition(*ast, nil, pos), nil
|
||||
}
|
||||
|
||||
func getBoardPathAtPosition(m d2ast.Map, currPath []string, pos d2ast.Position) []string {
|
||||
inRange := func(r d2ast.Range) bool {
|
||||
return !pos.Before(r.Start) && pos.Before(r.End)
|
||||
}
|
||||
|
||||
if !inRange(m.Range) {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, n := range m.Nodes {
|
||||
if n.MapKey == nil {
|
||||
continue
|
||||
}
|
||||
mk := n.MapKey
|
||||
|
||||
if mk.Key == nil || len(mk.Key.Path) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if mk.Value.Map == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
keyName := mk.Key.Path[0].Unbox().ScalarString()
|
||||
|
||||
if len(currPath)%2 == 0 {
|
||||
isBoardType := keyName == "layers" || keyName == "scenarios" || keyName == "steps"
|
||||
if !isBoardType {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if inRange(mk.Value.Map.Range) {
|
||||
newPath := append(currPath, keyName)
|
||||
|
||||
// Check deeper
|
||||
if deeperPath := getBoardPathAtPosition(*mk.Value.Map, newPath, pos); deeperPath != nil {
|
||||
return deeperPath
|
||||
}
|
||||
|
||||
// We're in between boards, e.g. layers.x.scenarios
|
||||
// Which means, there's no board at this position
|
||||
if len(newPath)%2 == 1 {
|
||||
return nil
|
||||
}
|
||||
// Nothing deeper matched but we're in this map's range, return current path
|
||||
return newPath
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
321
d2lsp/d2lsp_test.go
Normal file
321
d2lsp/d2lsp_test.go
Normal file
|
|
@ -0,0 +1,321 @@
|
|||
package d2lsp_test
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"oss.terrastruct.com/d2/d2ast"
|
||||
"oss.terrastruct.com/d2/d2lsp"
|
||||
"oss.terrastruct.com/util-go/assert"
|
||||
)
|
||||
|
||||
func TestGetFieldRanges(t *testing.T) {
|
||||
script := `x
|
||||
x.a
|
||||
a.x
|
||||
x -> y`
|
||||
fs := map[string]string{
|
||||
"index.d2": script,
|
||||
}
|
||||
ranges, _, err := d2lsp.GetRefRanges("index.d2", fs, nil, "x")
|
||||
assert.Success(t, err)
|
||||
assert.Equal(t, 3, len(ranges))
|
||||
assert.Equal(t, 0, ranges[0].Start.Line)
|
||||
assert.Equal(t, 1, ranges[1].Start.Line)
|
||||
assert.Equal(t, 3, ranges[2].Start.Line)
|
||||
|
||||
ranges, _, err = d2lsp.GetRefRanges("index.d2", fs, nil, "a.x")
|
||||
assert.Success(t, err)
|
||||
assert.Equal(t, 1, len(ranges))
|
||||
assert.Equal(t, 2, ranges[0].Start.Line)
|
||||
}
|
||||
|
||||
func TestGetEdgeRanges(t *testing.T) {
|
||||
script := `x
|
||||
x.a
|
||||
a.x
|
||||
x -> y
|
||||
y -> z
|
||||
x -> z
|
||||
b: {
|
||||
x -> y
|
||||
}
|
||||
`
|
||||
fs := map[string]string{
|
||||
"index.d2": script,
|
||||
}
|
||||
ranges, _, err := d2lsp.GetRefRanges("index.d2", fs, nil, "x -> y")
|
||||
assert.Success(t, err)
|
||||
assert.Equal(t, 1, len(ranges))
|
||||
assert.Equal(t, 3, ranges[0].Start.Line)
|
||||
|
||||
ranges, _, err = d2lsp.GetRefRanges("index.d2", fs, nil, "y -> z")
|
||||
assert.Success(t, err)
|
||||
assert.Equal(t, 1, len(ranges))
|
||||
assert.Equal(t, 4, ranges[0].Start.Line)
|
||||
|
||||
ranges, _, err = d2lsp.GetRefRanges("index.d2", fs, nil, "x -> z")
|
||||
assert.Success(t, err)
|
||||
assert.Equal(t, 1, len(ranges))
|
||||
assert.Equal(t, 5, ranges[0].Start.Line)
|
||||
|
||||
ranges, _, err = d2lsp.GetRefRanges("index.d2", fs, nil, "a -> b")
|
||||
assert.Success(t, err)
|
||||
assert.Equal(t, 0, len(ranges))
|
||||
|
||||
ranges, _, err = d2lsp.GetRefRanges("index.d2", fs, nil, "b.(x -> y)")
|
||||
assert.Success(t, err)
|
||||
assert.Equal(t, 1, len(ranges))
|
||||
assert.Equal(t, 7, ranges[0].Start.Line)
|
||||
}
|
||||
|
||||
func TestGetRangesImported(t *testing.T) {
|
||||
fs := map[string]string{
|
||||
"index.d2": `
|
||||
...@ok
|
||||
hi
|
||||
hey: @ok
|
||||
`,
|
||||
"ok.d2": `
|
||||
what
|
||||
lala
|
||||
okay
|
||||
`,
|
||||
}
|
||||
ranges, importRanges, err := d2lsp.GetRefRanges("index.d2", fs, nil, "hi")
|
||||
assert.Success(t, err)
|
||||
assert.Equal(t, 1, len(ranges))
|
||||
assert.Equal(t, 2, ranges[0].Start.Line)
|
||||
assert.Equal(t, 0, len(importRanges))
|
||||
|
||||
ranges, importRanges, err = d2lsp.GetRefRanges("index.d2", fs, nil, "okay")
|
||||
assert.Success(t, err)
|
||||
assert.Equal(t, 1, len(ranges))
|
||||
assert.Equal(t, "ok.d2", ranges[0].Path)
|
||||
assert.Equal(t, 1, len(importRanges))
|
||||
assert.Equal(t, 1, importRanges[0].Start.Line)
|
||||
|
||||
ranges, importRanges, err = d2lsp.GetRefRanges("index.d2", fs, nil, "hey.okay")
|
||||
assert.Success(t, err)
|
||||
assert.Equal(t, 1, len(ranges))
|
||||
assert.Equal(t, "ok.d2", ranges[0].Path)
|
||||
assert.Equal(t, 1, len(importRanges))
|
||||
assert.Equal(t, 3, importRanges[0].Start.Line)
|
||||
assert.Equal(t, 5, importRanges[0].Start.Column)
|
||||
|
||||
ranges, _, err = d2lsp.GetRefRanges("ok.d2", fs, nil, "hi")
|
||||
assert.Success(t, err)
|
||||
assert.Equal(t, 0, len(ranges))
|
||||
|
||||
ranges, _, err = d2lsp.GetRefRanges("ok.d2", fs, nil, "okay")
|
||||
assert.Success(t, err)
|
||||
assert.Equal(t, 1, len(ranges))
|
||||
}
|
||||
|
||||
func TestGetRangesBoards(t *testing.T) {
|
||||
fs := map[string]string{
|
||||
"index.d2": `
|
||||
hi
|
||||
layers: {
|
||||
x: {
|
||||
hello
|
||||
|
||||
layers: {
|
||||
y: {
|
||||
qwer
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
}
|
||||
ranges, _, err := d2lsp.GetRefRanges("index.d2", fs, []string{"x"}, "hello")
|
||||
assert.Success(t, err)
|
||||
assert.Equal(t, 1, len(ranges))
|
||||
assert.Equal(t, 4, ranges[0].Start.Line)
|
||||
|
||||
ranges, _, err = d2lsp.GetRefRanges("index.d2", fs, []string{"x"}, "hi")
|
||||
assert.Success(t, err)
|
||||
assert.Equal(t, 0, len(ranges))
|
||||
|
||||
ranges, _, err = d2lsp.GetRefRanges("index.d2", fs, []string{"x", "y"}, "qwer")
|
||||
assert.Success(t, err)
|
||||
assert.Equal(t, 1, len(ranges))
|
||||
|
||||
_, _, err = d2lsp.GetRefRanges("index.d2", fs, []string{"y"}, "hello")
|
||||
assert.Equal(t, `board "[y]" not found`, err.Error())
|
||||
}
|
||||
|
||||
func TestGetBoardAtPosition(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
fs map[string]string
|
||||
path string
|
||||
position d2ast.Position
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "cursor in layer",
|
||||
fs: map[string]string{
|
||||
"index.d2": `x
|
||||
layers: {
|
||||
basic: {
|
||||
x -> y
|
||||
}
|
||||
}`,
|
||||
},
|
||||
path: "index.d2",
|
||||
position: d2ast.Position{Line: 3, Column: 4},
|
||||
want: []string{"layers", "basic"},
|
||||
},
|
||||
{
|
||||
name: "cursor in nested layer",
|
||||
fs: map[string]string{
|
||||
"index.d2": `
|
||||
layers: {
|
||||
outer: {
|
||||
layers: {
|
||||
inner: {
|
||||
x -> y
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
},
|
||||
path: "index.d2",
|
||||
position: d2ast.Position{Line: 5, Column: 4},
|
||||
want: []string{"layers", "outer", "layers", "inner"},
|
||||
},
|
||||
{
|
||||
name: "cursor in second sibling nested layer",
|
||||
fs: map[string]string{
|
||||
"index.d2": `
|
||||
layers: {
|
||||
outer: {
|
||||
layers: {
|
||||
first: {
|
||||
a -> b
|
||||
}
|
||||
second: {
|
||||
x -> y
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
},
|
||||
path: "index.d2",
|
||||
position: d2ast.Position{Line: 8, Column: 4},
|
||||
want: []string{"layers", "outer", "layers", "second"},
|
||||
},
|
||||
{
|
||||
name: "cursor in root container",
|
||||
fs: map[string]string{
|
||||
"index.d2": `
|
||||
wumbo: {
|
||||
car
|
||||
}`,
|
||||
},
|
||||
path: "index.d2",
|
||||
position: d2ast.Position{Line: 2, Column: 4},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "cursor in layer container",
|
||||
fs: map[string]string{
|
||||
"index.d2": `
|
||||
layers: {
|
||||
x: {
|
||||
wumbo: {
|
||||
car
|
||||
}
|
||||
}
|
||||
}`,
|
||||
},
|
||||
path: "index.d2",
|
||||
position: d2ast.Position{Line: 4, Column: 4},
|
||||
want: []string{"layers", "x"},
|
||||
},
|
||||
{
|
||||
name: "cursor in scenario",
|
||||
fs: map[string]string{
|
||||
"index.d2": `
|
||||
scenarios: {
|
||||
happy: {
|
||||
x -> y
|
||||
}
|
||||
}`,
|
||||
},
|
||||
path: "index.d2",
|
||||
position: d2ast.Position{Line: 3, Column: 4},
|
||||
want: []string{"scenarios", "happy"},
|
||||
},
|
||||
{
|
||||
name: "cursor in step",
|
||||
fs: map[string]string{
|
||||
"index.d2": `
|
||||
steps: {
|
||||
first: {
|
||||
x -> y
|
||||
}
|
||||
}`,
|
||||
},
|
||||
path: "index.d2",
|
||||
position: d2ast.Position{Line: 3, Column: 4},
|
||||
want: []string{"steps", "first"},
|
||||
},
|
||||
{
|
||||
name: "cursor outside any board",
|
||||
fs: map[string]string{
|
||||
"index.d2": `
|
||||
x -> y
|
||||
layers: {
|
||||
basic: {
|
||||
a -> b
|
||||
}
|
||||
}`,
|
||||
},
|
||||
path: "index.d2",
|
||||
position: d2ast.Position{Line: 1, Column: 1},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "cursor in empty board",
|
||||
fs: map[string]string{
|
||||
"index.d2": `
|
||||
layers: {
|
||||
basic: {
|
||||
}
|
||||
}`,
|
||||
},
|
||||
path: "index.d2",
|
||||
position: d2ast.Position{Line: 3, Column: 2},
|
||||
want: []string{"layers", "basic"},
|
||||
},
|
||||
{
|
||||
name: "cursor in between",
|
||||
fs: map[string]string{
|
||||
"index.d2": `
|
||||
layers: {
|
||||
basic: {
|
||||
}
|
||||
}`,
|
||||
},
|
||||
path: "index.d2",
|
||||
position: d2ast.Position{Line: 2, Column: 2},
|
||||
want: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := d2lsp.GetBoardAtPosition(tt.fs[tt.path], tt.position)
|
||||
assert.Success(t, err)
|
||||
if tt.want == nil {
|
||||
assert.Equal(t, true, got == nil)
|
||||
} else {
|
||||
assert.Equal(t, len(tt.want), len(got))
|
||||
assert.True(t, slices.Equal(tt.want, got))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
125
d2oracle/edit.go
125
d2oracle/edit.go
|
|
@ -42,6 +42,9 @@ func Create(g *d2graph.Graph, boardPath []string, key string) (_ *d2graph.Graph,
|
|||
}
|
||||
// TODO beter name
|
||||
baseAST = boardG.BaseAST
|
||||
if baseAST == nil {
|
||||
return nil, "", fmt.Errorf("board %v cannot be modified through this file", boardPath)
|
||||
}
|
||||
}
|
||||
|
||||
newKey, edge, err := generateUniqueKey(boardG, key, nil, nil)
|
||||
|
|
@ -98,6 +101,9 @@ func Set(g *d2graph.Graph, boardPath []string, key string, tag, value *string) (
|
|||
}
|
||||
// TODO beter name
|
||||
baseAST = boardG.BaseAST
|
||||
if baseAST == nil {
|
||||
return nil, fmt.Errorf("board %v cannot be modified through this file", boardPath)
|
||||
}
|
||||
}
|
||||
|
||||
err = _set(boardG, baseAST, key, tag, value)
|
||||
|
|
@ -142,6 +148,9 @@ func ReconnectEdge(g *d2graph.Graph, boardPath []string, edgeKey string, srcKey,
|
|||
}
|
||||
// TODO beter name
|
||||
baseAST = boardG.BaseAST
|
||||
if baseAST == nil {
|
||||
return nil, fmt.Errorf("board %v cannot be modified through this file", boardPath)
|
||||
}
|
||||
}
|
||||
|
||||
obj := boardG.Root
|
||||
|
|
@ -372,7 +381,7 @@ func _set(g *d2graph.Graph, baseAST *d2ast.Map, key string, tag, value *string)
|
|||
if mk.Key != nil {
|
||||
found := true
|
||||
for _, idel := range d2graph.Key(mk.Key) {
|
||||
_, ok := d2graph.ReservedKeywords[idel]
|
||||
_, ok := d2ast.ReservedKeywords[idel]
|
||||
if ok {
|
||||
reserved = true
|
||||
break
|
||||
|
|
@ -411,6 +420,47 @@ func _set(g *d2graph.Graph, baseAST *d2ast.Map, key string, tag, value *string)
|
|||
}
|
||||
}
|
||||
|
||||
if mk.Key != nil && len(mk.Key.Path) == 2 {
|
||||
boardType := mk.Key.Path[0].Unbox().ScalarString()
|
||||
if boardType == "layers" || boardType == "scenarios" || boardType == "steps" {
|
||||
// Force map structure
|
||||
var containerMap *d2ast.Map
|
||||
for _, n := range scope.Nodes {
|
||||
if n.MapKey != nil && n.MapKey.Key != nil && len(n.MapKey.Key.Path) == 1 &&
|
||||
n.MapKey.Key.Path[0].Unbox().ScalarString() == boardType {
|
||||
containerMap = n.MapKey.Value.Map
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if containerMap == nil {
|
||||
containerMap = &d2ast.Map{
|
||||
Range: d2ast.MakeRange(",1:0:0-1:0:0"),
|
||||
}
|
||||
containerMK := &d2ast.Key{
|
||||
Key: &d2ast.KeyPath{
|
||||
Path: []*d2ast.StringBox{
|
||||
d2ast.MakeValueBox(d2ast.RawString(boardType, true)).StringBox(),
|
||||
},
|
||||
},
|
||||
Value: d2ast.MakeValueBox(containerMap),
|
||||
}
|
||||
appendMapKey(scope, containerMK)
|
||||
}
|
||||
|
||||
itemMK := &d2ast.Key{
|
||||
Key: &d2ast.KeyPath{
|
||||
Path: []*d2ast.StringBox{
|
||||
d2ast.MakeValueBox(d2ast.RawString(mk.Key.Path[1].Unbox().ScalarString(), true)).StringBox(),
|
||||
},
|
||||
},
|
||||
Value: mk.Value,
|
||||
}
|
||||
appendMapKey(containerMap, itemMK)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
writeableLabelMK := true
|
||||
var objK *d2ast.Key
|
||||
if baseAST != g.AST || imported {
|
||||
|
|
@ -450,7 +500,12 @@ func _set(g *d2graph.Graph, baseAST *d2ast.Map, key string, tag, value *string)
|
|||
m = obj.Map
|
||||
}
|
||||
|
||||
if (obj.Label.MapKey != nil && writeableLabelMK) && m == nil && (!found || reserved || len(mk.Edges) > 0) {
|
||||
if (obj.Label.MapKey != nil && writeableLabelMK) && m == nil && (!found || reserved || len(mk.Edges) > 0) &&
|
||||
// Label is not set like `hey.label: mylabel`
|
||||
// This should only work when label is set like `hey: mylabel`
|
||||
(obj.Label.MapKey.Key == nil ||
|
||||
len(obj.Label.MapKey.Key.Path) == 0 ||
|
||||
obj.Label.MapKey.Key.Path[len(obj.Label.MapKey.Key.Path)-1].Unbox().ScalarString() != "label") {
|
||||
m2 := &d2ast.Map{
|
||||
Range: d2ast.MakeRange(",1:0:0-1:0:0"),
|
||||
}
|
||||
|
|
@ -544,7 +599,7 @@ func _set(g *d2graph.Graph, baseAST *d2ast.Map, key string, tag, value *string)
|
|||
attrs = edge.Attributes
|
||||
|
||||
if mk.EdgeKey != nil {
|
||||
if _, ok := d2graph.ReservedKeywords[mk.EdgeKey.Path[0].Unbox().ScalarString()]; !ok {
|
||||
if _, ok := d2ast.ReservedKeywords[mk.EdgeKey.Path[0].Unbox().ScalarString()]; !ok {
|
||||
return errors.New("edge key must be reserved")
|
||||
}
|
||||
reserved = true
|
||||
|
|
@ -856,7 +911,7 @@ func appendMapKey(m *d2ast.Map, mk *d2ast.Key) {
|
|||
if len(m.Nodes) == 1 &&
|
||||
mk.Key != nil &&
|
||||
len(mk.Key.Path) > 0 {
|
||||
_, ok := d2graph.ReservedKeywords[mk.Key.Path[0].Unbox().ScalarString()]
|
||||
_, ok := d2ast.ReservedKeywords[mk.Key.Path[0].Unbox().ScalarString()]
|
||||
if ok {
|
||||
// Allow one line reserved key (like shape) maps.
|
||||
// TODO: This needs to be smarter as certain keys are only reserved in context.
|
||||
|
|
@ -900,6 +955,9 @@ func Delete(g *d2graph.Graph, boardPath []string, key string) (_ *d2graph.Graph,
|
|||
}
|
||||
// TODO beter name
|
||||
baseAST = boardG.BaseAST
|
||||
if baseAST == nil {
|
||||
return nil, fmt.Errorf("board %v cannot be modified through this file", boardPath)
|
||||
}
|
||||
}
|
||||
|
||||
g2, err := deleteReserved(g, boardPath, baseAST, mk)
|
||||
|
|
@ -949,7 +1007,10 @@ func Delete(g *d2graph.Graph, boardPath []string, key string) (_ *d2graph.Graph,
|
|||
|
||||
for i := len(e.References) - 1; i >= 0; i-- {
|
||||
ref := e.References[i]
|
||||
deleteEdge(g, ref.Scope, ref.MapKey, ref.MapKeyEdgeIndex)
|
||||
// Leave glob setters alone
|
||||
if !(ref.MapKey.EdgeIndex != nil && ref.MapKey.EdgeIndex.Glob) {
|
||||
deleteEdge(g, ref.Scope, ref.MapKey, ref.MapKeyEdgeIndex)
|
||||
}
|
||||
}
|
||||
|
||||
edges, ok := obj.FindEdges(mk)
|
||||
|
|
@ -981,7 +1042,10 @@ func Delete(g *d2graph.Graph, boardPath []string, key string) (_ *d2graph.Graph,
|
|||
return recompile(boardG)
|
||||
}
|
||||
|
||||
prevG, _ := recompile(boardG)
|
||||
prevG, err := recompile(boardG)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
obj, ok := boardG.Root.HasChild(d2graph.Key(mk.Key))
|
||||
if !ok {
|
||||
|
|
@ -1071,7 +1135,7 @@ func hoistRefChildren(g *d2graph.Graph, key *d2ast.KeyPath, ref d2graph.Referenc
|
|||
continue
|
||||
}
|
||||
if n.MapKey.Key != nil {
|
||||
_, ok := d2graph.ReservedKeywords[n.MapKey.Key.Path[0].Unbox().ScalarString()]
|
||||
_, ok := d2ast.ReservedKeywords[n.MapKey.Key.Path[0].Unbox().ScalarString()]
|
||||
if ok {
|
||||
continue
|
||||
}
|
||||
|
|
@ -1124,7 +1188,7 @@ func renameConflictsToParent(g *d2graph.Graph, key *d2ast.KeyPath) (*d2graph.Gra
|
|||
continue
|
||||
}
|
||||
if n.MapKey.Key != nil {
|
||||
_, ok := d2graph.ReservedKeywords[n.MapKey.Key.Path[0].Unbox().ScalarString()]
|
||||
_, ok := d2ast.ReservedKeywords[n.MapKey.Key.Path[0].Unbox().ScalarString()]
|
||||
if ok {
|
||||
continue
|
||||
}
|
||||
|
|
@ -1144,7 +1208,7 @@ func renameConflictsToParent(g *d2graph.Graph, key *d2ast.KeyPath) (*d2graph.Gra
|
|||
absKey.Path = append(absKey.Path, k.Path[0])
|
||||
absKeys = append(absKeys, absKey)
|
||||
}
|
||||
} else if _, ok := d2graph.ReservedKeywords[ref.Key.Path[len(ref.Key.Path)-1].Unbox().ScalarString()]; !ok {
|
||||
} else if _, ok := d2ast.ReservedKeywords[ref.Key.Path[len(ref.Key.Path)-1].Unbox().ScalarString()]; !ok {
|
||||
absKey, err := d2parser.ParseKey(ref.ScopeObj.AbsID())
|
||||
if err != nil {
|
||||
absKey = &d2ast.KeyPath{}
|
||||
|
|
@ -1248,7 +1312,7 @@ func deleteReserved(g *d2graph.Graph, boardPath []string, baseAST *d2ast.Map, mk
|
|||
}
|
||||
targetKey = mk.EdgeKey
|
||||
}
|
||||
_, ok := d2graph.ReservedKeywords[targetKey.Path[len(targetKey.Path)-1].Unbox().ScalarString()]
|
||||
_, ok := d2ast.ReservedKeywords[targetKey.Path[len(targetKey.Path)-1].Unbox().ScalarString()]
|
||||
if !ok {
|
||||
return g, nil
|
||||
}
|
||||
|
|
@ -1265,7 +1329,7 @@ func deleteReserved(g *d2graph.Graph, boardPath []string, baseAST *d2ast.Map, mk
|
|||
if len(mk.Edges) == 1 {
|
||||
if mk.Key != nil {
|
||||
var ok bool
|
||||
obj, ok = g.Root.HasChild(d2graph.Key(mk.Key))
|
||||
obj, ok = obj.HasChild(d2graph.Key(mk.Key))
|
||||
if !ok {
|
||||
return g, nil
|
||||
}
|
||||
|
|
@ -1291,7 +1355,7 @@ func deleteReserved(g *d2graph.Graph, boardPath []string, baseAST *d2ast.Map, mk
|
|||
imported := false
|
||||
parts := d2graph.Key(targetKey)
|
||||
for i, id := range parts {
|
||||
_, ok := d2graph.ReservedKeywords[id]
|
||||
_, ok := d2ast.ReservedKeywords[id]
|
||||
if ok {
|
||||
if id == "style" {
|
||||
isNestedKey = true
|
||||
|
|
@ -1469,7 +1533,7 @@ func deleteObject(g *d2graph.Graph, baseAST *d2ast.Map, key *d2ast.KeyPath, obj
|
|||
}
|
||||
ref.Key.Path = append(ref.Key.Path[:ref.KeyPathIndex], ref.Key.Path[ref.KeyPathIndex+1:]...)
|
||||
withoutSpecial := go2.Filter(ref.Key.Path, func(x *d2ast.StringBox) bool {
|
||||
_, isReserved := d2graph.ReservedKeywords[x.Unbox().ScalarString()]
|
||||
_, isReserved := d2ast.ReservedKeywords[x.Unbox().ScalarString()]
|
||||
isSpecial := isReserved || x.Unbox().ScalarString() == "_"
|
||||
return !isSpecial
|
||||
})
|
||||
|
|
@ -1488,7 +1552,7 @@ func deleteObject(g *d2graph.Graph, baseAST *d2ast.Map, key *d2ast.KeyPath, obj
|
|||
for i := 0; i < len(ref.MapKey.Value.Map.Nodes); i++ {
|
||||
n := ref.MapKey.Value.Map.Nodes[i]
|
||||
if n.MapKey != nil && n.MapKey.Key != nil {
|
||||
_, ok := d2graph.ReservedKeywords[n.MapKey.Key.Path[0].Unbox().ScalarString()]
|
||||
_, ok := d2ast.ReservedKeywords[n.MapKey.Key.Path[0].Unbox().ScalarString()]
|
||||
if ok {
|
||||
deleteFromMap(ref.MapKey.Value.Map, n.MapKey)
|
||||
i--
|
||||
|
|
@ -1655,7 +1719,7 @@ func Rename(g *d2graph.Graph, boardPath []string, key, newName string) (_ *d2gra
|
|||
mk2.Key = mk.Key
|
||||
mk = mk2
|
||||
} else {
|
||||
_, ok := d2graph.ReservedKeywords[newName]
|
||||
_, ok := d2ast.ReservedKeywords[newName]
|
||||
if ok {
|
||||
return nil, "", fmt.Errorf("cannot rename to reserved keyword: %#v", newName)
|
||||
}
|
||||
|
|
@ -1680,7 +1744,7 @@ func Rename(g *d2graph.Graph, boardPath []string, key, newName string) (_ *d2gra
|
|||
|
||||
func trimReservedSuffix(path []*d2ast.StringBox) []*d2ast.StringBox {
|
||||
for i, p := range path {
|
||||
if _, ok := d2graph.ReservedKeywords[p.Unbox().ScalarString()]; ok {
|
||||
if _, ok := d2ast.ReservedKeywords[p.Unbox().ScalarString()]; ok {
|
||||
return path[:i]
|
||||
}
|
||||
}
|
||||
|
|
@ -1709,6 +1773,9 @@ func move(g *d2graph.Graph, boardPath []string, key, newKey string, includeDesce
|
|||
}
|
||||
// TODO beter name
|
||||
baseAST = boardG.BaseAST
|
||||
if baseAST == nil {
|
||||
return nil, fmt.Errorf("board %v cannot be modified through this file", boardPath)
|
||||
}
|
||||
}
|
||||
|
||||
newKey, _, err := generateUniqueKey(boardG, newKey, nil, nil)
|
||||
|
|
@ -1758,7 +1825,10 @@ func move(g *d2graph.Graph, boardPath []string, key, newKey string, includeDesce
|
|||
return recompile(g)
|
||||
}
|
||||
|
||||
prevG, _ := recompile(boardG)
|
||||
prevG, err := recompile(boardG)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ak := d2graph.Key(mk.Key)
|
||||
ak2 := d2graph.Key(mk2.Key)
|
||||
|
|
@ -1924,9 +1994,9 @@ func move(g *d2graph.Graph, boardPath []string, key, newKey string, includeDesce
|
|||
ida = resolvedIDA
|
||||
}
|
||||
// e.g. "a.b.shape: circle"
|
||||
_, endsWithReserved := d2graph.ReservedKeywords[ida[len(ida)-1]]
|
||||
_, endsWithReserved := d2ast.ReservedKeywords[ida[len(ida)-1]]
|
||||
ida = go2.Filter(ida, func(x string) bool {
|
||||
_, ok := d2graph.ReservedKeywords[x]
|
||||
_, ok := d2ast.ReservedKeywords[x]
|
||||
return !ok
|
||||
})
|
||||
|
||||
|
|
@ -1967,7 +2037,7 @@ func move(g *d2graph.Graph, boardPath []string, key, newKey string, includeDesce
|
|||
continue
|
||||
}
|
||||
if n.MapKey.Key != nil {
|
||||
_, ok := d2graph.ReservedKeywords[n.MapKey.Key.Path[0].Unbox().ScalarString()]
|
||||
_, ok := d2ast.ReservedKeywords[n.MapKey.Key.Path[0].Unbox().ScalarString()]
|
||||
if ok {
|
||||
detachedMK.Value.Map.Nodes = append(detachedMK.Value.Map.Nodes, n)
|
||||
}
|
||||
|
|
@ -2213,7 +2283,7 @@ func filterReserved(value d2ast.ValueBox) (with, without d2ast.ValueBox) {
|
|||
forWithout = append(forWithout, n)
|
||||
continue
|
||||
}
|
||||
_, ok := d2graph.ReservedKeywords[n.MapKey.Key.Path[0].Unbox().ScalarString()]
|
||||
_, ok := d2ast.ReservedKeywords[n.MapKey.Key.Path[0].Unbox().ScalarString()]
|
||||
if !ok {
|
||||
flushComments(&forWithout)
|
||||
forWithout = append(forWithout, n)
|
||||
|
|
@ -2282,8 +2352,17 @@ func updateNear(prevG, g *d2graph.Graph, from, to *string, includeDescendants bo
|
|||
if len(n.MapKey.Key.Path) == 0 {
|
||||
continue
|
||||
}
|
||||
if len(n.MapKey.Key.Path) > 1 {
|
||||
if n.MapKey.Key.Path[len(n.MapKey.Key.Path)-2].Unbox().ScalarString() == "label" ||
|
||||
n.MapKey.Key.Path[len(n.MapKey.Key.Path)-2].Unbox().ScalarString() == "icon" {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if n.MapKey.Key.Path[len(n.MapKey.Key.Path)-1].Unbox().ScalarString() == "near" {
|
||||
k := n.MapKey.Value.ScalarBox().Unbox().ScalarString()
|
||||
if _, ok := d2ast.NearConstants[k]; ok {
|
||||
continue
|
||||
}
|
||||
if strings.EqualFold(k, *from) && to == nil {
|
||||
deleteFromMap(obj.Map, n.MapKey)
|
||||
} else {
|
||||
|
|
@ -2907,7 +2986,7 @@ func DeleteIDDeltas(g *d2graph.Graph, boardPath []string, key string) (deltas ma
|
|||
if mk.Key != nil {
|
||||
ida := d2graph.Key(mk.Key)
|
||||
// Deleting a reserved field cannot possibly have any deltas
|
||||
if _, ok := d2graph.ReservedKeywords[ida[len(ida)-1]]; ok {
|
||||
if _, ok := d2ast.ReservedKeywords[ida[len(ida)-1]]; ok {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
|
|
@ -3238,7 +3317,7 @@ func getMostNestedRefs(obj *d2graph.Object) []d2graph.Reference {
|
|||
|
||||
func filterReservedPath(path []*d2ast.StringBox) (filtered []*d2ast.StringBox) {
|
||||
for _, box := range path {
|
||||
if _, ok := d2graph.ReservedKeywords[strings.ToLower(box.Unbox().ScalarString())]; ok {
|
||||
if _, ok := d2ast.ReservedKeywords[strings.ToLower(box.Unbox().ScalarString())]; ok {
|
||||
return
|
||||
}
|
||||
filtered = append(filtered, box)
|
||||
|
|
|
|||
|
|
@ -482,6 +482,119 @@ layers: {
|
|||
b
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "add_layer/1",
|
||||
text: `b`,
|
||||
key: `layers.c`,
|
||||
|
||||
expKey: `layers.c`,
|
||||
exp: `b
|
||||
|
||||
layers: {
|
||||
c
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "add_layer/2",
|
||||
text: `b
|
||||
layers: {
|
||||
c: {
|
||||
x
|
||||
}
|
||||
}`,
|
||||
key: `layers.b`,
|
||||
|
||||
expKey: `layers.b`,
|
||||
exp: `b
|
||||
|
||||
layers: {
|
||||
c: {
|
||||
x
|
||||
}
|
||||
b
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "add_layer/3",
|
||||
text: `b
|
||||
|
||||
layers: {
|
||||
c: {
|
||||
d
|
||||
}
|
||||
}
|
||||
`,
|
||||
key: `layers.c`,
|
||||
|
||||
boardPath: []string{"c"},
|
||||
expKey: `layers.c`,
|
||||
exp: `b
|
||||
|
||||
layers: {
|
||||
c: {
|
||||
d
|
||||
|
||||
layers: {
|
||||
c
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "add_layer/4",
|
||||
text: `b
|
||||
|
||||
layers: {
|
||||
c
|
||||
}
|
||||
`,
|
||||
key: `d`,
|
||||
|
||||
boardPath: []string{"c"},
|
||||
expKey: `d`,
|
||||
exp: `b
|
||||
|
||||
layers: {
|
||||
c: {
|
||||
d
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "add_layer/5",
|
||||
text: `classes: {
|
||||
a: {
|
||||
style.stroke: red
|
||||
}
|
||||
}
|
||||
b
|
||||
|
||||
layers: {
|
||||
c
|
||||
}
|
||||
`,
|
||||
key: `d`,
|
||||
|
||||
boardPath: []string{"c"},
|
||||
expKey: `d`,
|
||||
exp: `classes: {
|
||||
a: {
|
||||
style.stroke: red
|
||||
}
|
||||
}
|
||||
b
|
||||
|
||||
layers: {
|
||||
c: {
|
||||
d
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
|
|
@ -941,6 +1054,16 @@ square.style.opacity: 0.2
|
|||
key: `square.top`,
|
||||
value: go2.Pointer(`200`),
|
||||
exp: `square: {top: 200}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "labeled_set_position",
|
||||
text: `hey.label: what
|
||||
`,
|
||||
key: `hey.top`,
|
||||
value: go2.Pointer(`200`),
|
||||
exp: `hey.label: what
|
||||
hey.top: 200
|
||||
`,
|
||||
},
|
||||
{
|
||||
|
|
@ -2344,6 +2467,28 @@ layers: {
|
|||
(a -> b)[0].style.stroke: red
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "import/10",
|
||||
|
||||
text: `heyn
|
||||
|
||||
layers: {
|
||||
man: {...@meow}
|
||||
}
|
||||
`,
|
||||
fsTexts: map[string]string{
|
||||
"meow.d2": `layers: {
|
||||
1: {
|
||||
asdf
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
boardPath: []string{"man", "1"},
|
||||
key: `asdf.link`,
|
||||
value: go2.Pointer(`_._`),
|
||||
expErr: `failed to set "asdf.link" to "\"_._\"": board [man 1] cannot be modified through this file`,
|
||||
},
|
||||
{
|
||||
name: "label-near/1",
|
||||
|
||||
|
|
@ -2518,6 +2663,87 @@ x -> a.b -> a.b.c
|
|||
# hi
|
||||
style.stroke: green
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "scenario-child",
|
||||
|
||||
text: `a -> b
|
||||
|
||||
scenarios: {
|
||||
x: {
|
||||
hi
|
||||
}
|
||||
}
|
||||
`,
|
||||
key: `(a -> b)[0].style.stroke-width`,
|
||||
value: go2.Pointer(`3`),
|
||||
boardPath: []string{"x"},
|
||||
exp: `a -> b
|
||||
|
||||
scenarios: {
|
||||
x: {
|
||||
hi
|
||||
(a -> b)[0].style.stroke-width: 3
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "scenario-grandchild",
|
||||
|
||||
text: `a -> b
|
||||
|
||||
scenarios: {
|
||||
x: {
|
||||
scenarios: {
|
||||
c: {
|
||||
(a -> b)[0].style.bold: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
key: `(a -> b)[0].style.stroke-width`,
|
||||
value: go2.Pointer(`3`),
|
||||
boardPath: []string{"x", "c"},
|
||||
exp: `a -> b
|
||||
|
||||
scenarios: {
|
||||
x: {
|
||||
scenarios: {
|
||||
c: {
|
||||
(a -> b)[0].style.bold: true
|
||||
(a -> b)[0].style.stroke-width: 3
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "step-connection",
|
||||
|
||||
text: `steps: {
|
||||
1: {
|
||||
Modules -- Metricbeat: {
|
||||
style.stroke-width: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
`,
|
||||
key: `Metricbeat.style.stroke`,
|
||||
value: go2.Pointer(`red`),
|
||||
boardPath: []string{"1"},
|
||||
exp: `steps: {
|
||||
1: {
|
||||
Modules -- Metricbeat: {
|
||||
style.stroke-width: 1
|
||||
}
|
||||
Metricbeat.style.stroke: red
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
|
@ -7759,6 +7985,124 @@ layers: {
|
|||
a
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "edge-out-layer",
|
||||
|
||||
text: `x: {
|
||||
a -> b
|
||||
}
|
||||
`,
|
||||
key: `x.(a -> b)[0].style.stroke`,
|
||||
exp: `x: {
|
||||
a -> b
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "edge-in-layer",
|
||||
|
||||
text: `layers: {
|
||||
test: {
|
||||
x: {
|
||||
a -> b
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
boardPath: []string{"test"},
|
||||
key: `x.(a -> b)[0].style.stroke`,
|
||||
exp: `layers: {
|
||||
test: {
|
||||
x: {
|
||||
a -> b
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "label-near-in-layer",
|
||||
|
||||
text: `layers: {
|
||||
x: {
|
||||
y: {
|
||||
label.near: center-center
|
||||
}
|
||||
a
|
||||
}
|
||||
}
|
||||
`,
|
||||
boardPath: []string{"x"},
|
||||
key: `y`,
|
||||
exp: `layers: {
|
||||
x: {
|
||||
a
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "update-near-in-layer",
|
||||
|
||||
text: `layers: {
|
||||
x: {
|
||||
y: {
|
||||
near: a
|
||||
}
|
||||
a
|
||||
}
|
||||
}
|
||||
`,
|
||||
boardPath: []string{"x"},
|
||||
key: `y`,
|
||||
exp: `layers: {
|
||||
x: {
|
||||
a
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "edge-with-glob",
|
||||
|
||||
text: `x -> y
|
||||
y
|
||||
|
||||
(* -> *)[*].style.opacity: 0.8
|
||||
`,
|
||||
key: `(x -> y)[0]`,
|
||||
exp: `x
|
||||
y
|
||||
|
||||
(* -> *)[*].style.opacity: 0.8
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "layer-delete-complex-object",
|
||||
|
||||
text: `k
|
||||
|
||||
layers: {
|
||||
x: {
|
||||
a: "b" {
|
||||
top: 184
|
||||
left: 180
|
||||
}
|
||||
j
|
||||
}
|
||||
}
|
||||
`,
|
||||
key: `a`,
|
||||
boardPath: []string{"x"},
|
||||
exp: `k
|
||||
|
||||
layers: {
|
||||
x: {
|
||||
j
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -153,11 +153,12 @@ func IsImportedObj(ast *d2ast.Map, obj *d2graph.Object) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
// Globs count as imported for now
|
||||
// Glob creations count as imported for now
|
||||
// TODO Probably rename later
|
||||
func IsImportedEdge(ast *d2ast.Map, edge *d2graph.Edge) bool {
|
||||
for _, ref := range edge.References {
|
||||
if ref.Edge.Src.HasGlob() || ref.Edge.Dst.HasGlob() {
|
||||
// If edge index, the glob is just setting something, not responsible for creating the edge
|
||||
if (ref.Edge.Src.HasGlob() || ref.Edge.Dst.HasGlob()) && ref.MapKey.EdgeIndex == nil {
|
||||
return true
|
||||
}
|
||||
if ref.Edge.Range.Path != ast.Range.Path {
|
||||
|
|
@ -248,7 +249,7 @@ func GetID(key string) string {
|
|||
|
||||
func GetWriteableRefs(obj *d2graph.Object, writeableAST *d2ast.Map) (out []d2graph.Reference) {
|
||||
for i, ref := range obj.References {
|
||||
if ref.ScopeAST == writeableAST && ref.Key.Range.Path == writeableAST.Range.Path {
|
||||
if ref.ScopeAST == writeableAST && ref.Key.Range.Path == writeableAST.Range.Path && len(ref.MapKey.Edges) == 0 {
|
||||
out = append(out, obj.References[i])
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1163,16 +1163,18 @@ func (p *parser) parseUnquotedString(inKey bool) (s *d2ast.UnquotedString) {
|
|||
sb.WriteRune(r)
|
||||
rawb.WriteRune(r)
|
||||
r = r2
|
||||
case '*':
|
||||
if sb.Len() == 0 {
|
||||
s.Pattern = append(s.Pattern, "*")
|
||||
} else {
|
||||
s.Pattern = append(s.Pattern, sb.String()[lastPatternIndex:], "*")
|
||||
}
|
||||
lastPatternIndex = len(sb.String()) + 1
|
||||
}
|
||||
}
|
||||
|
||||
if r == '*' {
|
||||
if sb.Len() == 0 {
|
||||
s.Pattern = append(s.Pattern, "*")
|
||||
} else {
|
||||
s.Pattern = append(s.Pattern, sb.String()[lastPatternIndex:], "*")
|
||||
}
|
||||
lastPatternIndex = len(sb.String()) + 1
|
||||
}
|
||||
|
||||
p.commit()
|
||||
|
||||
if !unicode.IsSpace(r) {
|
||||
|
|
|
|||
|
|
@ -54,13 +54,21 @@ func Wrap(rootDiagram *d2target.Diagram, svgs [][]byte, renderOpts d2svg.RenderO
|
|||
width := br.X - tl.X + int(*renderOpts.Pad)*2
|
||||
height := br.Y - tl.Y + int(*renderOpts.Pad)*2
|
||||
|
||||
fitToScreenWrapperOpening := fmt.Sprintf(`<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="%s" preserveAspectRatio="xMinYMin meet" viewBox="0 0 %d %d">`,
|
||||
var dimensions string
|
||||
if renderOpts.Scale != nil {
|
||||
dimensions = fmt.Sprintf(` width="%d" height="%d"`,
|
||||
int(math.Ceil((*renderOpts.Scale)*float64(width))),
|
||||
int(math.Ceil((*renderOpts.Scale)*float64(height))),
|
||||
)
|
||||
}
|
||||
|
||||
fitToScreenWrapperOpening := fmt.Sprintf(`<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="%s" preserveAspectRatio="xMinYMin meet" viewBox="0 0 %d %d"%s>`,
|
||||
version.Version,
|
||||
width, height,
|
||||
width, height, dimensions,
|
||||
)
|
||||
fmt.Fprint(buf, fitToScreenWrapperOpening)
|
||||
|
||||
innerOpening := fmt.Sprintf(`<svg id="d2-svg" width="%d" height="%d" viewBox="%d %d %d %d">`,
|
||||
innerOpening := fmt.Sprintf(`<svg class="d2-svg" width="%d" height="%d" viewBox="%d %d %d %d">`,
|
||||
width, height, left, top, width, height)
|
||||
fmt.Fprint(buf, innerOpening)
|
||||
|
||||
|
|
@ -69,7 +77,7 @@ func Wrap(rootDiagram *d2target.Diagram, svgs [][]byte, renderOpts d2svg.RenderO
|
|||
svgsStr += string(svg) + " "
|
||||
}
|
||||
|
||||
diagramHash, err := rootDiagram.HashID()
|
||||
diagramHash, err := rootDiagram.HashID(renderOpts.Salt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -94,7 +102,7 @@ func Wrap(rootDiagram *d2target.Diagram, svgs [][]byte, renderOpts d2svg.RenderO
|
|||
}
|
||||
|
||||
if renderOpts.Sketch != nil && *renderOpts.Sketch {
|
||||
d2sketch.DefineFillPatterns(buf)
|
||||
d2sketch.DefineFillPatterns(buf, diagramHash)
|
||||
}
|
||||
|
||||
fmt.Fprint(buf, `<style type="text/css"><![CDATA[`)
|
||||
|
|
|
|||
|
|
@ -6,9 +6,9 @@ import (
|
|||
"math"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
|
||||
"oss.terrastruct.com/d2/lib/jsrunner"
|
||||
"oss.terrastruct.com/util-go/xdefer"
|
||||
)
|
||||
|
||||
|
|
@ -29,21 +29,25 @@ var svgRe = regexp.MustCompile(`<svg[^>]+width="([0-9\.]+)ex" height="([0-9\.]+)
|
|||
|
||||
func Render(s string) (_ string, err error) {
|
||||
defer xdefer.Errorf(&err, "latex failed to parse")
|
||||
vm := goja.New()
|
||||
s = doubleBackslashes(s)
|
||||
runner := jsrunner.NewJSRunner()
|
||||
|
||||
if _, err := vm.RunString(polyfillsJS); err != nil {
|
||||
if _, err := runner.RunString(polyfillsJS); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if _, err := vm.RunString(mathjaxJS); err != nil {
|
||||
if _, err := runner.RunString(mathjaxJS); err != nil {
|
||||
// Known issue that a harmless error occurs in JS: https://github.com/mathjax/MathJax/issues/3289
|
||||
if runner.Engine() == jsrunner.Goja {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := runner.RunString(setupJS); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if _, err := vm.RunString(setupJS); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
val, err := vm.RunString(fmt.Sprintf(`adaptor.innerHTML(html.convert(`+"`"+"%s`"+`, {
|
||||
val, err := runner.RunString(fmt.Sprintf(`adaptor.innerHTML(html.convert(`+"`"+"%s`"+`, {
|
||||
em: %d,
|
||||
ex: %d,
|
||||
}))`, s, pxPerEx*2, pxPerEx))
|
||||
|
|
@ -80,3 +84,15 @@ func Measure(s string) (width, height int, err error) {
|
|||
|
||||
return int(math.Ceil(wf * float64(pxPerEx))), int(math.Ceil(hf * float64(pxPerEx))), nil
|
||||
}
|
||||
|
||||
func doubleBackslashes(s string) string {
|
||||
var result strings.Builder
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] == '\\' {
|
||||
result.WriteString("\\\\")
|
||||
} else {
|
||||
result.WriteByte(s[i])
|
||||
}
|
||||
}
|
||||
return result.String()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import (
|
|||
func TestRender(t *testing.T) {
|
||||
txts := []string{
|
||||
`a + b = c`,
|
||||
`\\frac{1}{2}`,
|
||||
`\frac{1}{2}`,
|
||||
`a + b
|
||||
= c
|
||||
`,
|
||||
|
|
@ -24,10 +24,3 @@ func TestRender(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderError(t *testing.T) {
|
||||
_, err := Render(`\frac{1}{2}`)
|
||||
if err == nil {
|
||||
t.Fatal("expected to error on invalid latex syntax")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,5 +2,10 @@ const adaptor = MathJax._.adaptors.liteAdaptor.liteAdaptor();
|
|||
MathJax._.handlers.html_ts.RegisterHTMLHandler(adaptor)
|
||||
const html = MathJax._.mathjax.mathjax.document('', {
|
||||
InputJax: new MathJax._.input.tex_ts.TeX({ packages: ['base', 'mathtools', 'ams', 'amscd', 'braket', 'cancel', 'cases', 'color', 'gensymb', 'mhchem', 'physics'] }),
|
||||
OutputJax: new MathJax._.output.svg_ts.SVG(),
|
||||
OutputJax: new MathJax._.output.svg_ts.SVG({fontCache: 'none'}),
|
||||
});
|
||||
|
||||
if (typeof globalThis !== 'undefined') {
|
||||
globalThis.adaptor = adaptor;
|
||||
globalThis.html = html;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,3 +17,7 @@ const root = {
|
|||
};
|
||||
const rc = rough.svg(root, { seed: 1 });
|
||||
let node;
|
||||
|
||||
if (typeof globalThis !== "undefined") {
|
||||
globalThis.rc = rc;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,12 +9,11 @@ import (
|
|||
|
||||
_ "embed"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
|
||||
"oss.terrastruct.com/d2/d2target"
|
||||
"oss.terrastruct.com/d2/d2themes"
|
||||
"oss.terrastruct.com/d2/lib/color"
|
||||
"oss.terrastruct.com/d2/lib/geo"
|
||||
"oss.terrastruct.com/d2/lib/jsrunner"
|
||||
"oss.terrastruct.com/d2/lib/label"
|
||||
"oss.terrastruct.com/d2/lib/svg"
|
||||
"oss.terrastruct.com/util-go/go2"
|
||||
|
|
@ -29,8 +28,6 @@ var setupJS string
|
|||
//go:embed streaks.txt
|
||||
var streaks string
|
||||
|
||||
type Runner goja.Runtime
|
||||
|
||||
var baseRoughProps = `fillWeight: 2.0,
|
||||
hachureGap: 16,
|
||||
fillStyle: "solid",
|
||||
|
|
@ -44,46 +41,39 @@ const (
|
|||
FG_COLOR = color.N1
|
||||
)
|
||||
|
||||
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
|
||||
func LoadJS(runner jsrunner.JSRunner) error {
|
||||
if _, err := runner.RunString(roughJS); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := vm.RunString(setupJS); err != nil {
|
||||
return nil, err
|
||||
if _, err := runner.RunString(setupJS); err != nil {
|
||||
return err
|
||||
}
|
||||
r := Runner(*vm)
|
||||
return &r, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// DefineFillPatterns adds reusable patterns that are overlayed on shapes with
|
||||
// fill. This gives it a subtle streaky effect that subtly looks hand-drawn but
|
||||
// not distractingly so.
|
||||
func DefineFillPatterns(buf *bytes.Buffer) {
|
||||
func DefineFillPatterns(buf *bytes.Buffer, diagramHash string) {
|
||||
source := buf.String()
|
||||
fmt.Fprint(buf, "<defs>")
|
||||
|
||||
defineFillPattern(buf, source, "bright", "rgba(0, 0, 0, 0.1)")
|
||||
defineFillPattern(buf, source, "normal", "rgba(0, 0, 0, 0.16)")
|
||||
defineFillPattern(buf, source, "dark", "rgba(0, 0, 0, 0.32)")
|
||||
defineFillPattern(buf, source, "darker", "rgba(255, 255, 255, 0.24)")
|
||||
defineFillPattern(buf, source, diagramHash, "bright", "rgba(0, 0, 0, 0.1)")
|
||||
defineFillPattern(buf, source, diagramHash, "normal", "rgba(0, 0, 0, 0.16)")
|
||||
defineFillPattern(buf, source, diagramHash, "dark", "rgba(0, 0, 0, 0.32)")
|
||||
defineFillPattern(buf, source, diagramHash, "darker", "rgba(255, 255, 255, 0.24)")
|
||||
|
||||
fmt.Fprint(buf, "</defs>")
|
||||
}
|
||||
|
||||
func defineFillPattern(buf *bytes.Buffer, source string, luminanceCategory, fill string) {
|
||||
trigger := fmt.Sprintf(`url(#streaks-%s)`, luminanceCategory)
|
||||
func defineFillPattern(buf *bytes.Buffer, source, diagramHash string, luminanceCategory, fill string) {
|
||||
trigger := fmt.Sprintf(`url(#streaks-%s-%s)`, luminanceCategory, diagramHash)
|
||||
if strings.Contains(source, trigger) {
|
||||
fmt.Fprintf(buf, streaks, luminanceCategory, fill)
|
||||
fmt.Fprintf(buf, streaks, luminanceCategory, diagramHash, fill)
|
||||
}
|
||||
}
|
||||
|
||||
func Rect(r *Runner, shape d2target.Shape) (string, error) {
|
||||
func Rect(r jsrunner.JSRunner, shape d2target.Shape) (string, error) {
|
||||
js := fmt.Sprintf(`node = rc.rectangle(0, 0, %d, %d, {
|
||||
fill: "#000",
|
||||
stroke: "#000",
|
||||
|
|
@ -95,7 +85,7 @@ func Rect(r *Runner, shape d2target.Shape) (string, error) {
|
|||
return "", err
|
||||
}
|
||||
output := ""
|
||||
pathEl := d2themes.NewThemableElement("path")
|
||||
pathEl := d2themes.NewThemableElement("path", nil)
|
||||
pathEl.SetTranslate(float64(shape.Pos.X), float64(shape.Pos.Y))
|
||||
pathEl.Fill, pathEl.Stroke = d2themes.ShapeTheme(shape)
|
||||
pathEl.FillPattern = shape.FillPattern
|
||||
|
|
@ -106,7 +96,7 @@ func Rect(r *Runner, shape d2target.Shape) (string, error) {
|
|||
output += pathEl.Render()
|
||||
}
|
||||
|
||||
sketchOEl := d2themes.NewThemableElement("rect")
|
||||
sketchOEl := d2themes.NewThemableElement("rect", nil)
|
||||
sketchOEl.SetTranslate(float64(shape.Pos.X), float64(shape.Pos.Y))
|
||||
sketchOEl.Width = float64(shape.Width)
|
||||
sketchOEl.Height = float64(shape.Height)
|
||||
|
|
@ -119,7 +109,7 @@ func Rect(r *Runner, shape d2target.Shape) (string, error) {
|
|||
return output, nil
|
||||
}
|
||||
|
||||
func DoubleRect(r *Runner, shape d2target.Shape) (string, error) {
|
||||
func DoubleRect(r jsrunner.JSRunner, shape d2target.Shape) (string, error) {
|
||||
jsBigRect := fmt.Sprintf(`node = rc.rectangle(0, 0, %d, %d, {
|
||||
fill: "#000",
|
||||
stroke: "#000",
|
||||
|
|
@ -143,7 +133,7 @@ func DoubleRect(r *Runner, shape d2target.Shape) (string, error) {
|
|||
|
||||
output := ""
|
||||
|
||||
pathEl := d2themes.NewThemableElement("path")
|
||||
pathEl := d2themes.NewThemableElement("path", nil)
|
||||
pathEl.SetTranslate(float64(shape.Pos.X), float64(shape.Pos.Y))
|
||||
pathEl.Fill, pathEl.Stroke = d2themes.ShapeTheme(shape)
|
||||
pathEl.FillPattern = shape.FillPattern
|
||||
|
|
@ -154,7 +144,7 @@ func DoubleRect(r *Runner, shape d2target.Shape) (string, error) {
|
|||
output += pathEl.Render()
|
||||
}
|
||||
|
||||
pathEl = d2themes.NewThemableElement("path")
|
||||
pathEl = d2themes.NewThemableElement("path", nil)
|
||||
pathEl.SetTranslate(float64(shape.Pos.X+d2target.INNER_BORDER_OFFSET), float64(shape.Pos.Y+d2target.INNER_BORDER_OFFSET))
|
||||
pathEl.Fill, pathEl.Stroke = d2themes.ShapeTheme(shape)
|
||||
// No need for inner to double paint
|
||||
|
|
@ -166,7 +156,7 @@ func DoubleRect(r *Runner, shape d2target.Shape) (string, error) {
|
|||
output += pathEl.Render()
|
||||
}
|
||||
|
||||
sketchOEl := d2themes.NewThemableElement("rect")
|
||||
sketchOEl := d2themes.NewThemableElement("rect", nil)
|
||||
sketchOEl.SetTranslate(float64(shape.Pos.X), float64(shape.Pos.Y))
|
||||
sketchOEl.Width = float64(shape.Width)
|
||||
sketchOEl.Height = float64(shape.Height)
|
||||
|
|
@ -179,7 +169,7 @@ func DoubleRect(r *Runner, shape d2target.Shape) (string, error) {
|
|||
return output, nil
|
||||
}
|
||||
|
||||
func Oval(r *Runner, shape d2target.Shape) (string, error) {
|
||||
func Oval(r jsrunner.JSRunner, shape d2target.Shape) (string, error) {
|
||||
js := fmt.Sprintf(`node = rc.ellipse(%d, %d, %d, %d, {
|
||||
fill: "#000",
|
||||
stroke: "#000",
|
||||
|
|
@ -191,7 +181,7 @@ func Oval(r *Runner, shape d2target.Shape) (string, error) {
|
|||
return "", err
|
||||
}
|
||||
output := ""
|
||||
pathEl := d2themes.NewThemableElement("path")
|
||||
pathEl := d2themes.NewThemableElement("path", nil)
|
||||
pathEl.SetTranslate(float64(shape.Pos.X), float64(shape.Pos.Y))
|
||||
pathEl.Fill, pathEl.Stroke = d2themes.ShapeTheme(shape)
|
||||
pathEl.FillPattern = shape.FillPattern
|
||||
|
|
@ -202,7 +192,7 @@ func Oval(r *Runner, shape d2target.Shape) (string, error) {
|
|||
output += pathEl.Render()
|
||||
}
|
||||
|
||||
soElement := d2themes.NewThemableElement("ellipse")
|
||||
soElement := d2themes.NewThemableElement("ellipse", nil)
|
||||
soElement.SetTranslate(float64(shape.Pos.X+shape.Width/2), float64(shape.Pos.Y+shape.Height/2))
|
||||
soElement.Rx = float64(shape.Width / 2)
|
||||
soElement.Ry = float64(shape.Height / 2)
|
||||
|
|
@ -218,7 +208,7 @@ func Oval(r *Runner, shape d2target.Shape) (string, error) {
|
|||
return output, nil
|
||||
}
|
||||
|
||||
func DoubleOval(r *Runner, shape d2target.Shape) (string, error) {
|
||||
func DoubleOval(r jsrunner.JSRunner, shape d2target.Shape) (string, error) {
|
||||
jsBigCircle := fmt.Sprintf(`node = rc.ellipse(%d, %d, %d, %d, {
|
||||
fill: "#000",
|
||||
stroke: "#000",
|
||||
|
|
@ -242,7 +232,7 @@ func DoubleOval(r *Runner, shape d2target.Shape) (string, error) {
|
|||
|
||||
output := ""
|
||||
|
||||
pathEl := d2themes.NewThemableElement("path")
|
||||
pathEl := d2themes.NewThemableElement("path", nil)
|
||||
pathEl.SetTranslate(float64(shape.Pos.X), float64(shape.Pos.Y))
|
||||
pathEl.Fill, pathEl.Stroke = d2themes.ShapeTheme(shape)
|
||||
pathEl.FillPattern = shape.FillPattern
|
||||
|
|
@ -253,7 +243,7 @@ func DoubleOval(r *Runner, shape d2target.Shape) (string, error) {
|
|||
output += pathEl.Render()
|
||||
}
|
||||
|
||||
pathEl = d2themes.NewThemableElement("path")
|
||||
pathEl = d2themes.NewThemableElement("path", nil)
|
||||
pathEl.SetTranslate(float64(shape.Pos.X), float64(shape.Pos.Y))
|
||||
pathEl.Fill, pathEl.Stroke = d2themes.ShapeTheme(shape)
|
||||
// No need for inner to double paint
|
||||
|
|
@ -264,7 +254,7 @@ func DoubleOval(r *Runner, shape d2target.Shape) (string, error) {
|
|||
pathEl.D = p
|
||||
output += pathEl.Render()
|
||||
}
|
||||
soElement := d2themes.NewThemableElement("ellipse")
|
||||
soElement := d2themes.NewThemableElement("ellipse", nil)
|
||||
soElement.SetTranslate(float64(shape.Pos.X+shape.Width/2), float64(shape.Pos.Y+shape.Height/2))
|
||||
soElement.Rx = float64(shape.Width / 2)
|
||||
soElement.Ry = float64(shape.Height / 2)
|
||||
|
|
@ -281,7 +271,7 @@ func DoubleOval(r *Runner, shape d2target.Shape) (string, error) {
|
|||
}
|
||||
|
||||
// TODO need to personalize this per shape like we do in Terrastruct app
|
||||
func Paths(r *Runner, shape d2target.Shape, paths []string) (string, error) {
|
||||
func Paths(r jsrunner.JSRunner, shape d2target.Shape, paths []string) (string, error) {
|
||||
output := ""
|
||||
for _, path := range paths {
|
||||
js := fmt.Sprintf(`node = rc.path("%s", {
|
||||
|
|
@ -294,7 +284,7 @@ func Paths(r *Runner, shape d2target.Shape, paths []string) (string, error) {
|
|||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
pathEl := d2themes.NewThemableElement("path")
|
||||
pathEl := d2themes.NewThemableElement("path", nil)
|
||||
pathEl.Fill, pathEl.Stroke = d2themes.ShapeTheme(shape)
|
||||
pathEl.FillPattern = shape.FillPattern
|
||||
pathEl.ClassName = "shape"
|
||||
|
|
@ -304,7 +294,7 @@ func Paths(r *Runner, shape d2target.Shape, paths []string) (string, error) {
|
|||
output += pathEl.Render()
|
||||
}
|
||||
|
||||
soElement := d2themes.NewThemableElement("path")
|
||||
soElement := d2themes.NewThemableElement("path", nil)
|
||||
for _, p := range sketchPaths {
|
||||
soElement.D = p
|
||||
renderedSO, err := d2themes.NewThemableSketchOverlay(
|
||||
|
|
@ -320,34 +310,75 @@ func Paths(r *Runner, shape d2target.Shape, paths []string) (string, error) {
|
|||
return output, nil
|
||||
}
|
||||
|
||||
func Connection(r *Runner, connection d2target.Connection, path, attrs string) (string, error) {
|
||||
roughness := 0.5
|
||||
js := fmt.Sprintf(`node = rc.path("%s", {roughness: %f, seed: 1});`, path, roughness)
|
||||
paths, err := computeRoughPathData(r, js)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
output := ""
|
||||
func Connection(r jsrunner.JSRunner, connection d2target.Connection, path, attrs string) (string, error) {
|
||||
animatedClass := ""
|
||||
if connection.Animated {
|
||||
animatedClass = " animated-connection"
|
||||
}
|
||||
|
||||
pathEl := d2themes.NewThemableElement("path")
|
||||
pathEl.Fill = color.None
|
||||
pathEl.Stroke = connection.Stroke
|
||||
pathEl.ClassName = fmt.Sprintf("connection%s", animatedClass)
|
||||
pathEl.Style = connection.CSSStyle()
|
||||
pathEl.Attributes = attrs
|
||||
for _, p := range paths {
|
||||
pathEl.D = p
|
||||
output += pathEl.Render()
|
||||
if connection.Animated {
|
||||
// If connection is animated and bidirectional
|
||||
if (connection.DstArrow == d2target.NoArrowhead && connection.SrcArrow == d2target.NoArrowhead) || (connection.DstArrow != d2target.NoArrowhead && connection.SrcArrow != d2target.NoArrowhead) {
|
||||
// There is no pure CSS way to animate bidirectional connections in two directions, so we split it up
|
||||
path1, path2, err := svg.SplitPath(path, 0.5)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
pathEl1 := d2themes.NewThemableElement("path", nil)
|
||||
pathEl1.D = path1
|
||||
pathEl1.Fill = color.None
|
||||
pathEl1.Stroke = connection.Stroke
|
||||
pathEl1.ClassName = fmt.Sprintf("connection%s", animatedClass)
|
||||
pathEl1.Style = connection.CSSStyle()
|
||||
pathEl1.Style += "animation-direction: reverse;"
|
||||
pathEl1.Attributes = attrs
|
||||
|
||||
pathEl2 := d2themes.NewThemableElement("path", nil)
|
||||
pathEl2.D = path2
|
||||
pathEl2.Fill = color.None
|
||||
pathEl2.Stroke = connection.Stroke
|
||||
pathEl2.ClassName = fmt.Sprintf("connection%s", animatedClass)
|
||||
pathEl2.Style = connection.CSSStyle()
|
||||
pathEl2.Attributes = attrs
|
||||
return pathEl1.Render() + " " + pathEl2.Render(), nil
|
||||
} else {
|
||||
pathEl := d2themes.NewThemableElement("path", nil)
|
||||
pathEl.D = path
|
||||
pathEl.Fill = color.None
|
||||
pathEl.Stroke = connection.Stroke
|
||||
pathEl.ClassName = fmt.Sprintf("connection%s", animatedClass)
|
||||
pathEl.Style = connection.CSSStyle()
|
||||
pathEl.Attributes = attrs
|
||||
return pathEl.Render(), nil
|
||||
}
|
||||
} else {
|
||||
roughness := 0.5
|
||||
js := fmt.Sprintf(`node = rc.path("%s", {roughness: %f, seed: 1});`, path, roughness)
|
||||
paths, err := computeRoughPathData(r, js)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
output := ""
|
||||
|
||||
pathEl := d2themes.NewThemableElement("path", nil)
|
||||
pathEl.Fill = color.None
|
||||
pathEl.Stroke = connection.Stroke
|
||||
pathEl.ClassName = fmt.Sprintf("connection%s", animatedClass)
|
||||
pathEl.Style = connection.CSSStyle()
|
||||
pathEl.Attributes = attrs
|
||||
for _, p := range paths {
|
||||
pathEl.D = p
|
||||
output += pathEl.Render()
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
|
||||
// TODO cleanup
|
||||
func Table(r *Runner, shape d2target.Shape) (string, error) {
|
||||
func Table(r jsrunner.JSRunner, shape d2target.Shape) (string, error) {
|
||||
output := ""
|
||||
js := fmt.Sprintf(`node = rc.rectangle(0, 0, %d, %d, {
|
||||
fill: "#000",
|
||||
|
|
@ -359,7 +390,7 @@ func Table(r *Runner, shape d2target.Shape) (string, error) {
|
|||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
pathEl := d2themes.NewThemableElement("path")
|
||||
pathEl := d2themes.NewThemableElement("path", nil)
|
||||
pathEl.SetTranslate(float64(shape.Pos.X), float64(shape.Pos.Y))
|
||||
pathEl.Fill, pathEl.Stroke = d2themes.ShapeTheme(shape)
|
||||
pathEl.FillPattern = shape.FillPattern
|
||||
|
|
@ -386,7 +417,7 @@ func Table(r *Runner, shape d2target.Shape) (string, error) {
|
|||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
pathEl = d2themes.NewThemableElement("path")
|
||||
pathEl = d2themes.NewThemableElement("path", nil)
|
||||
pathEl.SetTranslate(float64(shape.Pos.X), float64(shape.Pos.Y))
|
||||
pathEl.Fill = shape.Fill
|
||||
pathEl.FillPattern = shape.FillPattern
|
||||
|
|
@ -404,7 +435,7 @@ func Table(r *Runner, shape d2target.Shape) (string, error) {
|
|||
float64(shape.LabelHeight),
|
||||
)
|
||||
|
||||
textEl := d2themes.NewThemableElement("text")
|
||||
textEl := d2themes.NewThemableElement("text", nil)
|
||||
textEl.X = tl.X
|
||||
textEl.Y = tl.Y + float64(shape.LabelHeight)*3/4
|
||||
textEl.Fill = shape.GetFontColor()
|
||||
|
|
@ -437,7 +468,7 @@ func Table(r *Runner, shape d2target.Shape) (string, error) {
|
|||
float64(shape.FontSize),
|
||||
)
|
||||
|
||||
textEl := d2themes.NewThemableElement("text")
|
||||
textEl := d2themes.NewThemableElement("text", nil)
|
||||
textEl.X = nameTL.X
|
||||
textEl.Y = nameTL.Y + float64(shape.FontSize)*3/4
|
||||
textEl.Fill = shape.PrimaryAccentColor
|
||||
|
|
@ -467,7 +498,7 @@ func Table(r *Runner, shape d2target.Shape) (string, error) {
|
|||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
pathEl := d2themes.NewThemableElement("path")
|
||||
pathEl := d2themes.NewThemableElement("path", nil)
|
||||
pathEl.Fill = shape.Fill
|
||||
pathEl.FillPattern = shape.FillPattern
|
||||
for _, p := range paths {
|
||||
|
|
@ -476,7 +507,7 @@ func Table(r *Runner, shape d2target.Shape) (string, error) {
|
|||
}
|
||||
}
|
||||
|
||||
sketchOEl := d2themes.NewThemableElement("rect")
|
||||
sketchOEl := d2themes.NewThemableElement("rect", nil)
|
||||
sketchOEl.SetTranslate(float64(shape.Pos.X), float64(shape.Pos.Y))
|
||||
sketchOEl.Width = float64(shape.Width)
|
||||
sketchOEl.Height = float64(shape.Height)
|
||||
|
|
@ -489,7 +520,7 @@ func Table(r *Runner, shape d2target.Shape) (string, error) {
|
|||
return output, nil
|
||||
}
|
||||
|
||||
func Class(r *Runner, shape d2target.Shape) (string, error) {
|
||||
func Class(r jsrunner.JSRunner, shape d2target.Shape) (string, error) {
|
||||
output := ""
|
||||
js := fmt.Sprintf(`node = rc.rectangle(0, 0, %d, %d, {
|
||||
fill: "#000",
|
||||
|
|
@ -501,7 +532,7 @@ func Class(r *Runner, shape d2target.Shape) (string, error) {
|
|||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
pathEl := d2themes.NewThemableElement("path")
|
||||
pathEl := d2themes.NewThemableElement("path", nil)
|
||||
pathEl.SetTranslate(float64(shape.Pos.X), float64(shape.Pos.Y))
|
||||
pathEl.Fill, pathEl.Stroke = d2themes.ShapeTheme(shape)
|
||||
pathEl.FillPattern = shape.FillPattern
|
||||
|
|
@ -529,7 +560,7 @@ func Class(r *Runner, shape d2target.Shape) (string, error) {
|
|||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
pathEl = d2themes.NewThemableElement("path")
|
||||
pathEl = d2themes.NewThemableElement("path", nil)
|
||||
pathEl.SetTranslate(float64(shape.Pos.X), float64(shape.Pos.Y))
|
||||
pathEl.Fill = shape.Fill
|
||||
pathEl.FillPattern = shape.FillPattern
|
||||
|
|
@ -539,7 +570,7 @@ func Class(r *Runner, shape d2target.Shape) (string, error) {
|
|||
output += pathEl.Render()
|
||||
}
|
||||
|
||||
sketchOEl := d2themes.NewThemableElement("rect")
|
||||
sketchOEl := d2themes.NewThemableElement("rect", nil)
|
||||
sketchOEl.SetTranslate(float64(shape.Pos.X), float64(shape.Pos.Y))
|
||||
sketchOEl.Width = float64(shape.Width)
|
||||
sketchOEl.Height = headerBox.Height
|
||||
|
|
@ -557,7 +588,7 @@ func Class(r *Runner, shape d2target.Shape) (string, error) {
|
|||
float64(shape.LabelHeight),
|
||||
)
|
||||
|
||||
textEl := d2themes.NewThemableElement("text")
|
||||
textEl := d2themes.NewThemableElement("text", nil)
|
||||
textEl.X = tl.X + float64(shape.LabelWidth)/2
|
||||
textEl.Y = tl.Y + float64(shape.LabelHeight)*3/4
|
||||
textEl.Fill = shape.GetFontColor()
|
||||
|
|
@ -584,7 +615,7 @@ func Class(r *Runner, shape d2target.Shape) (string, error) {
|
|||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
pathEl = d2themes.NewThemableElement("path")
|
||||
pathEl = d2themes.NewThemableElement("path", nil)
|
||||
pathEl.Fill = shape.Fill
|
||||
pathEl.FillPattern = shape.FillPattern
|
||||
pathEl.ClassName = "class_header"
|
||||
|
|
@ -616,7 +647,7 @@ func classRow(shape d2target.Shape, box *geo.Box, prefix, nameText, typeText str
|
|||
fontSize,
|
||||
)
|
||||
|
||||
textEl := d2themes.NewThemableElement("text")
|
||||
textEl := d2themes.NewThemableElement("text", nil)
|
||||
textEl.X = prefixTL.X
|
||||
textEl.Y = prefixTL.Y + fontSize*3/4
|
||||
textEl.Fill = shape.PrimaryAccentColor
|
||||
|
|
@ -640,8 +671,8 @@ func classRow(shape d2target.Shape, box *geo.Box, prefix, nameText, typeText str
|
|||
return output
|
||||
}
|
||||
|
||||
func computeRoughPathData(r *Runner, js string) ([]string, error) {
|
||||
if _, err := r.run(js); err != nil {
|
||||
func computeRoughPathData(r jsrunner.JSRunner, js string) ([]string, error) {
|
||||
if _, err := r.RunString(js); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
roughPaths, err := extractRoughPaths(r)
|
||||
|
|
@ -651,8 +682,8 @@ func computeRoughPathData(r *Runner, js string) ([]string, error) {
|
|||
return extractPathData(roughPaths)
|
||||
}
|
||||
|
||||
func computeRoughPaths(r *Runner, js string) ([]roughPath, error) {
|
||||
if _, err := r.run(js); err != nil {
|
||||
func computeRoughPaths(r jsrunner.JSRunner, js string) ([]roughPath, error) {
|
||||
if _, err := r.RunString(js); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return extractRoughPaths(r)
|
||||
|
|
@ -681,8 +712,8 @@ func (rp roughPath) StyleCSS() string {
|
|||
return style
|
||||
}
|
||||
|
||||
func extractRoughPaths(r *Runner) ([]roughPath, error) {
|
||||
val, err := r.run("JSON.stringify(node.children, null, ' ')")
|
||||
func extractRoughPaths(r jsrunner.JSRunner) ([]roughPath, error) {
|
||||
val, err := r.RunString("JSON.stringify(node.children, null, ' ')")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -715,7 +746,7 @@ func extractPathData(roughPaths []roughPath) ([]string, error) {
|
|||
return paths, nil
|
||||
}
|
||||
|
||||
func ArrowheadJS(r *Runner, arrowhead d2target.Arrowhead, stroke string, strokeWidth int) (arrowJS, extraJS string) {
|
||||
func ArrowheadJS(r jsrunner.JSRunner, arrowhead d2target.Arrowhead, stroke string, strokeWidth int) (arrowJS, extraJS string) {
|
||||
// Note: selected each seed that looks the good for consistent renders
|
||||
switch arrowhead {
|
||||
case d2target.ArrowArrowhead:
|
||||
|
|
@ -802,11 +833,34 @@ func ArrowheadJS(r *Runner, arrowhead d2target.Arrowhead, stroke string, strokeW
|
|||
stroke,
|
||||
BG_COLOR,
|
||||
)
|
||||
case d2target.CircleArrowhead:
|
||||
arrowJS = fmt.Sprintf(
|
||||
`node = rc.circle(-2, -1, 8, { strokeWidth: %d, stroke: "%s", fill: "%s", fillStyle: "solid", fillWeight: 1, seed: 5 })`,
|
||||
strokeWidth,
|
||||
stroke,
|
||||
BG_COLOR,
|
||||
)
|
||||
case d2target.BoxArrowhead:
|
||||
arrowJS = fmt.Sprintf(
|
||||
`node = rc.polygon(%s, { strokeWidth: %d, stroke: "%s", fill: "%s", fillStyle: "solid", seed: 1})`,
|
||||
`[[0, -10], [0, 10], [-20, 10], [-20, -10]]`,
|
||||
strokeWidth,
|
||||
stroke,
|
||||
BG_COLOR,
|
||||
)
|
||||
case d2target.FilledBoxArrowhead:
|
||||
arrowJS = fmt.Sprintf(
|
||||
`node = rc.polygon(%s, { strokeWidth: %d, stroke: "%s", fill: "%s", fillStyle: "solid", seed: 1})`,
|
||||
`[[0, -10], [0, 10], [-20, 10], [-20, -10]]`,
|
||||
strokeWidth,
|
||||
stroke,
|
||||
stroke,
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func Arrowheads(r *Runner, connection d2target.Connection, srcAdj, dstAdj *geo.Point) (string, error) {
|
||||
func Arrowheads(r jsrunner.JSRunner, connection d2target.Connection, srcAdj, dstAdj *geo.Point) (string, error) {
|
||||
arrowPaths := []string{}
|
||||
|
||||
if connection.SrcArrow != d2target.NoArrowhead {
|
||||
|
|
@ -835,7 +889,7 @@ func Arrowheads(r *Runner, connection d2target.Connection, srcAdj, dstAdj *geo.P
|
|||
roughPaths = append(roughPaths, extraPaths...)
|
||||
}
|
||||
|
||||
pathEl := d2themes.NewThemableElement("path")
|
||||
pathEl := d2themes.NewThemableElement("path", nil)
|
||||
pathEl.ClassName = "connection"
|
||||
pathEl.Attributes = transform
|
||||
for _, rp := range roughPaths {
|
||||
|
|
@ -874,7 +928,7 @@ func Arrowheads(r *Runner, connection d2target.Connection, srcAdj, dstAdj *geo.P
|
|||
roughPaths = append(roughPaths, extraPaths...)
|
||||
}
|
||||
|
||||
pathEl := d2themes.NewThemableElement("path")
|
||||
pathEl := d2themes.NewThemableElement("path", nil)
|
||||
pathEl.ClassName = "connection"
|
||||
pathEl.Attributes = transform
|
||||
for _, rp := range roughPaths {
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue