Merge branch 'master' into add-cross-support
This commit is contained in:
commit
de2de0f4df
2238 changed files with 304717 additions and 115448 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
|
||||
|
|
|
|||
36
.github/workflows/daily.yml
vendored
36
.github/workflows/daily.yml
vendored
|
|
@ -8,7 +8,41 @@ concurrency:
|
|||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
npm-nightly:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0 # Needed for git history and version tags
|
||||
|
||||
- name: Check for changes
|
||||
id: check_changes
|
||||
run: |
|
||||
if [ $(git rev-list --count --since="24 hours ago" HEAD) -gt 0 ]; then
|
||||
echo "has_changes=true" >> $GITHUB_OUTPUT
|
||||
echo "Found changes in the last 24 hours, proceeding to publish d2js nightly"
|
||||
else
|
||||
echo "has_changes=false" >> $GITHUB_OUTPUT
|
||||
echo "No changes in the last 24 hours, skipping d2js nightly publish"
|
||||
fi
|
||||
|
||||
- uses: actions/setup-go@v4
|
||||
if: steps.check_changes.outputs.has_changes == 'true'
|
||||
with:
|
||||
go-version-file: ./go.mod
|
||||
cache: true
|
||||
|
||||
- name: Publish nightly version to NPM
|
||||
if: steps.check_changes.outputs.has_changes == 'true'
|
||||
run: |
|
||||
export NPM_VERSION=nightly
|
||||
COLOR=1 ./make.sh js
|
||||
env:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets._GITHUB_TOKEN }}
|
||||
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||
ci:
|
||||
needs: [npm-nightly]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
|
@ -20,7 +54,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 && NPM_VERSION="${NPM_VERSION}" prefix "$@" ./make.sh all
|
||||
|
|
|
|||
13
README.md
13
README.md
|
|
@ -4,11 +4,13 @@
|
|||
A modern diagram scripting language that turns text to diagrams.
|
||||
</h2>
|
||||
|
||||
[Docs](https://d2lang.com) | [Cheat sheet](./docs/assets/cheat_sheet.pdf) | [Comparisons](https://text-to-diagram.com) | [Playground](https://play.d2lang.com)
|
||||
[Docs](https://d2lang.com) | [Cheat sheet](./docs/assets/cheat_sheet.pdf) | [Comparisons](https://text-to-diagram.com) | [Playground](https://play.d2lang.com) | [IDE](https://app.terrastruct.com)
|
||||
|
||||
[](https://github.com/terrastruct/d2/actions/workflows/ci.yml)
|
||||
[](https://github.com/terrastruct/d2/actions/workflows/daily.yml)
|
||||
[](https://github.com/terrastruct/d2/releases)
|
||||
[](./CHANGELOG.md)
|
||||
[](https://www.npmjs.com/package/@terrastruct/d2)
|
||||
[](https://discord.gg/NF6X8K4eDq)
|
||||
[](https://twitter.com/terrastruct)
|
||||
[](./LICENSE.txt)
|
||||
|
|
@ -16,6 +18,9 @@
|
|||
<a href="https://play.d2lang.com">
|
||||
<img src="./docs/assets/playground_button.png" alt="D2 Playground button" width="200" />
|
||||
</a>
|
||||
<a href="https://app.terrastruct.com">
|
||||
<img src="./docs/assets/studio_button.png" alt="D2 Studio button" width="200" />
|
||||
</a>
|
||||
|
||||
https://user-images.githubusercontent.com/3120367/206125010-bd1fea8e-248a-43e7-8f85-0bbfca0c6e2a.mp4
|
||||
|
||||
|
|
@ -238,7 +243,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)
|
||||
|
|
@ -261,12 +266,16 @@ let us know and we'll be happy to include it here!
|
|||
- **ent2d2**: [https://github.com/tmc/ent2d2](https://github.com/tmc/ent2d2)
|
||||
- **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)
|
||||
- **Hexo blog extension**: [https://github.com/leverimmy/hexo-d2](https://github.com/leverimmy/hexo-d2)
|
||||
- **Rehype Plugin**: [https://github.com/stereobooster/beoe/tree/main/packages/rehype-d2](https://github.com/stereobooster/beoe/tree/main/packages/rehype-d2)
|
||||
|
||||
### Misc
|
||||
|
||||
- **Comparison site**: [https://github.com/terrastruct/text-to-diagram-site](https://github.com/terrastruct/text-to-diagram-site)
|
||||
- **Playground**: [https://github.com/terrastruct/d2-playground](https://github.com/terrastruct/d2-playground)
|
||||
- **IDE (paid)**: [https://app.terrastruct.com](https://app.terrastruct.com)
|
||||
- **Language docs**: [https://github.com/terrastruct/d2-docs](https://github.com/terrastruct/d2-docs)
|
||||
- **Hosted icons**: [https://icons.terrastruct.com](https://icons.terrastruct.com)
|
||||
|
||||
|
|
|
|||
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
|
||||
|
|
@ -4,7 +4,7 @@ cd -- "$(dirname "$0")/../.."
|
|||
. ./ci/sub/lib.sh
|
||||
|
||||
tag="$(sh_c docker build \
|
||||
--build-arg GOVERSION="1.22.2.linux-$ARCH" \
|
||||
--build-arg GOVERSION="1.23.6.linux-$ARCH" \
|
||||
-qf ./ci/release/linux/Dockerfile ./ci/release/linux)"
|
||||
docker_run \
|
||||
-e DRY_RUN \
|
||||
|
|
|
|||
|
|
@ -1,13 +1,9 @@
|
|||
#### 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)
|
||||
|
||||
#### Bugfixes ⛑️
|
||||
|
||||
- Render: fixes edge case of a 3d shape with outside label being cut off [#2132](https://github.com/terrastruct/d2/pull/2132)
|
||||
---
|
||||
|
||||
For the latest d2.js changes, see separate [changelog](https://github.com/terrastruct/d2/blob/master/d2js/js/CHANGELOG.md).
|
||||
|
|
|
|||
|
|
@ -3,3 +3,7 @@
|
|||
#### Improvements 🧹
|
||||
|
||||
#### Bugfixes ⛑️
|
||||
|
||||
---
|
||||
|
||||
For the latest d2.js changes, see separate [changelog](https://github.com/terrastruct/d2/blob/master/d2js/js/CHANGELOG.md).
|
||||
|
|
|
|||
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)
|
||||
35
ci/release/changelogs/v0.6.9.md
Normal file
35
ci/release/changelogs/v0.6.9.md
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
#### 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)
|
||||
59
ci/release/changelogs/v0.7.0.md
Normal file
59
ci/release/changelogs/v0.7.0.md
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
#### Features 🚀
|
||||
|
||||
- Icons:
|
||||
- connections can include icons [#12](https://github.com/terrastruct/d2/issues/12)
|
||||
- Syntax:
|
||||
- `suspend`/`unsuspend` to define models and instantiate them [#2394](https://github.com/terrastruct/d2/pull/2394)
|
||||
- Globs:
|
||||
- support for filtering edges based on properties of endpoint nodes (e.g., `&src.style.fill: blue`) [#2395](https://github.com/terrastruct/d2/pull/2395)
|
||||
- `level` filter implemented [#2473](https://github.com/terrastruct/d2/pull/2473)
|
||||
- Render:
|
||||
- markdown, latex, and code can be used as object labels [#2204](https://github.com/terrastruct/d2/pull/2204)
|
||||
- `shape: c4-person` to render a person shape like what the C4 model prescribes [#2397](https://github.com/terrastruct/d2/pull/2397)
|
||||
- Icons:
|
||||
- border-radius should work on icon [#2409](https://github.com/terrastruct/d2/issues/2409)
|
||||
- Misc:
|
||||
- Diagram legends are implemented [#2416](https://github.com/terrastruct/d2/pull/2416)
|
||||
|
||||
#### Improvements 🧹
|
||||
|
||||
- CLI:
|
||||
- Support `validate` command. [#2415](https://github.com/terrastruct/d2/pull/2415)
|
||||
- Watch mode ignores backup files (e.g. files created by certain editors like Helix). [#2131](https://github.com/terrastruct/d2/issues/2131)
|
||||
- Support for `--omit-version` flag. [#2377](https://github.com/terrastruct/d2/issues/2377)
|
||||
- Casing is ignored for plugin names [#2486](https://github.com/terrastruct/d2/pull/2486)
|
||||
- Compiler:
|
||||
- `link`s can be set to root path, e.g. `/xyz`. [#2357](https://github.com/terrastruct/d2/issues/2357)
|
||||
- When importing a file, attempt resolving substitutions at the imported file scope first [#2482](https://github.com/terrastruct/d2/pull/2482)
|
||||
- validate gradient color stops. [#2492](https://github.com/terrastruct/d2/pull/2492)
|
||||
- Parser:
|
||||
- impose max key length. It's almost certainly a mistake if an ID gets too long, e.g. missing quotes [#2465](https://github.com/terrastruct/d2/pull/2465)
|
||||
- Render:
|
||||
- horizontal padding added for connection labels [#2461](https://github.com/terrastruct/d2/pull/2461)
|
||||
|
||||
#### Bugfixes ⛑️
|
||||
|
||||
- Compiler:
|
||||
- fixes panic when `sql_shape` shape value had mixed casing [#2349](https://github.com/terrastruct/d2/pull/2349)
|
||||
- fixes panic when importing from a file with spread substitutions in `vars` [#2427](https://github.com/terrastruct/d2/pull/2427)
|
||||
- fixes support for `center` in `d2-config` [#2360](https://github.com/terrastruct/d2/pull/2360)
|
||||
- fixes panic when comment lines appear in arrays [#2378](https://github.com/terrastruct/d2/pull/2378)
|
||||
- fixes inconsistencies when objects were double quoted [#2390](https://github.com/terrastruct/d2/pull/2390)
|
||||
- fixes globs not applying to spread substitutions [#2426](https://github.com/terrastruct/d2/issues/2426)
|
||||
- fixes panic when classes were mixed with layers incorrectly [#2448](https://github.com/terrastruct/d2/pull/2448)
|
||||
- fixes panic when gradient colors are used in sketch mode [#2481](https://github.com/terrastruct/d2/pull/2487)
|
||||
- fixes panic using glob ampersand filters with composite values [#2489](https://github.com/terrastruct/d2/pull/2489)
|
||||
- fixes leaf ampersand filter when used with imports [#2494](https://github.com/terrastruct/d2/pull/2494)
|
||||
- Formatter:
|
||||
- fixes substitutions in quotes surrounded by text [#2462](https://github.com/terrastruct/d2/pull/2462)
|
||||
- CLI:
|
||||
- fetch and render remote images of mimetype octet-stream correctly [#2370](https://github.com/terrastruct/d2/pull/2370)
|
||||
- Composition:
|
||||
- spread importing scenarios/steps was not inheriting correctly [#2460](https://github.com/terrastruct/d2/pull/2460)
|
||||
- imported fields were not merging with current fields/edges [#2464](https://github.com/terrastruct/d2/pull/2464)
|
||||
- Markdown:
|
||||
- fixes nested var substitutions not working [#2456](https://github.com/terrastruct/d2/pull/2456)
|
||||
|
||||
---
|
||||
|
||||
For the latest d2.js changes, see separate [changelog](https://github.com/terrastruct/d2/blob/master/d2js/js/CHANGELOG.md).
|
||||
39
ci/release/release-js.sh
Executable file
39
ci/release/release-js.sh
Executable file
|
|
@ -0,0 +1,39 @@
|
|||
#!/bin/sh
|
||||
set -eu
|
||||
cd -- "$(dirname "$0")/../.."
|
||||
. "./ci/sub/lib.sh"
|
||||
|
||||
VERSION=""
|
||||
|
||||
help() {
|
||||
cat <<EOF
|
||||
usage: $0 --version=<version>
|
||||
|
||||
Publishes the d2.js to NPM.
|
||||
|
||||
Flags:
|
||||
--version Version to publish (e.g., "0.1.2" or "nightly"). Note this is the js version, not related to the d2 version. A non-nightly version will publish to latest.
|
||||
EOF
|
||||
}
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--help|-h)
|
||||
help
|
||||
exit 0
|
||||
;;
|
||||
--version=*)
|
||||
VERSION="${arg#*=}"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ -z "$VERSION" ]; then
|
||||
flag_errusage "--version is required"
|
||||
fi
|
||||
|
||||
FGCOLOR=6 header "Publishing JavaScript package to NPM (version: $VERSION)"
|
||||
|
||||
sh_c "NPM_VERSION=$VERSION ./make.sh js"
|
||||
|
||||
FGCOLOR=2 header 'NPM publish completed'
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
#!/bin/sh
|
||||
set -eu
|
||||
cd -- "$(dirname "$0")/../.."
|
||||
. "./ci/sub/lib.sh"
|
||||
|
||||
./ci/sub/release/release.sh "$@"
|
||||
|
|
|
|||
|
|
@ -8,12 +8,17 @@
|
|||
.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
|
||||
.Nm d2
|
||||
.Ar validate Ar file.d2
|
||||
.Sh DESCRIPTION
|
||||
.Nm
|
||||
compiles and renders
|
||||
|
|
@ -125,12 +130,27 @@ 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 .
|
||||
.It Fl -omit-version Ar false
|
||||
omit D2 version from generated image
|
||||
.Ns .
|
||||
.El
|
||||
.Sh SUBCOMMANDS
|
||||
.Bl -tag -width Fl
|
||||
|
|
@ -145,7 +165,64 @@ 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)
|
||||
.It Ar validate Ar file.d2
|
||||
Validates file.d2
|
||||
.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.
|
||||
.It Ev Sy OMIT_VERSION
|
||||
See --omit-version
|
||||
.El
|
||||
.Sh SEE ALSO
|
||||
.Xr d2plugin-tala 1
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ var _ Node = &Comment{}
|
|||
var _ Node = &BlockComment{}
|
||||
|
||||
var _ Node = &Null{}
|
||||
var _ Node = &Suspension{}
|
||||
var _ Node = &Boolean{}
|
||||
var _ Node = &Number{}
|
||||
var _ Node = &UnquotedString{}
|
||||
|
|
@ -166,7 +167,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{}
|
||||
|
|
@ -276,7 +278,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.
|
||||
|
|
@ -322,6 +330,7 @@ type Scalar interface {
|
|||
|
||||
// See String for rest.
|
||||
var _ Scalar = &Null{}
|
||||
var _ Scalar = &Suspension{}
|
||||
var _ Scalar = &Boolean{}
|
||||
var _ Scalar = &Number{}
|
||||
|
||||
|
|
@ -331,6 +340,7 @@ type String interface {
|
|||
SetString(string)
|
||||
Copy() String
|
||||
_string()
|
||||
IsUnquoted() bool
|
||||
}
|
||||
|
||||
var _ String = &UnquotedString{}
|
||||
|
|
@ -341,6 +351,7 @@ var _ String = &BlockString{}
|
|||
func (c *Comment) node() {}
|
||||
func (c *BlockComment) node() {}
|
||||
func (n *Null) node() {}
|
||||
func (n *Suspension) node() {}
|
||||
func (b *Boolean) node() {}
|
||||
func (n *Number) node() {}
|
||||
func (s *UnquotedString) node() {}
|
||||
|
|
@ -359,6 +370,7 @@ func (i *EdgeIndex) node() {}
|
|||
func (c *Comment) Type() string { return "comment" }
|
||||
func (c *BlockComment) Type() string { return "block comment" }
|
||||
func (n *Null) Type() string { return "null" }
|
||||
func (n *Suspension) Type() string { return "suspension" }
|
||||
func (b *Boolean) Type() string { return "boolean" }
|
||||
func (n *Number) Type() string { return "number" }
|
||||
func (s *UnquotedString) Type() string { return "unquoted string" }
|
||||
|
|
@ -377,6 +389,7 @@ func (i *EdgeIndex) Type() string { return "edge index" }
|
|||
func (c *Comment) GetRange() Range { return c.Range }
|
||||
func (c *BlockComment) GetRange() Range { return c.Range }
|
||||
func (n *Null) GetRange() Range { return n.Range }
|
||||
func (n *Suspension) GetRange() Range { return n.Range }
|
||||
func (b *Boolean) GetRange() Range { return b.Range }
|
||||
func (n *Number) GetRange() Range { return n.Range }
|
||||
func (s *UnquotedString) GetRange() Range { return s.Range }
|
||||
|
|
@ -401,6 +414,7 @@ func (i *Import) mapNode() {}
|
|||
func (c *Comment) arrayNode() {}
|
||||
func (c *BlockComment) arrayNode() {}
|
||||
func (n *Null) arrayNode() {}
|
||||
func (n *Suspension) arrayNode() {}
|
||||
func (b *Boolean) arrayNode() {}
|
||||
func (n *Number) arrayNode() {}
|
||||
func (s *UnquotedString) arrayNode() {}
|
||||
|
|
@ -413,6 +427,7 @@ func (a *Array) arrayNode() {}
|
|||
func (m *Map) arrayNode() {}
|
||||
|
||||
func (n *Null) value() {}
|
||||
func (n *Suspension) value() {}
|
||||
func (b *Boolean) value() {}
|
||||
func (n *Number) value() {}
|
||||
func (s *UnquotedString) value() {}
|
||||
|
|
@ -424,6 +439,7 @@ func (m *Map) value() {}
|
|||
func (i *Import) value() {}
|
||||
|
||||
func (n *Null) scalar() {}
|
||||
func (n *Suspension) scalar() {}
|
||||
func (b *Boolean) scalar() {}
|
||||
func (n *Number) scalar() {}
|
||||
func (s *UnquotedString) scalar() {}
|
||||
|
|
@ -434,6 +450,7 @@ 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 (n *Suspension) 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 }
|
||||
|
|
@ -565,9 +582,10 @@ func Walk(node Node, fn func(Node) bool) {
|
|||
}
|
||||
|
||||
// TODO: mistake, move into parse.go
|
||||
func (n *Null) ScalarString() string { return "" }
|
||||
func (b *Boolean) ScalarString() string { return strconv.FormatBool(b.Value) }
|
||||
func (n *Number) ScalarString() string { return n.Raw }
|
||||
func (n *Null) ScalarString() string { return "" }
|
||||
func (n *Suspension) ScalarString() string { return "" }
|
||||
func (b *Boolean) ScalarString() string { return strconv.FormatBool(b.Value) }
|
||||
func (n *Number) ScalarString() string { return n.Raw }
|
||||
func (s *UnquotedString) ScalarString() string {
|
||||
if len(s.Value) == 0 {
|
||||
return ""
|
||||
|
|
@ -604,6 +622,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"`
|
||||
|
|
@ -618,6 +641,11 @@ type Null struct {
|
|||
Range Range `json:"range"`
|
||||
}
|
||||
|
||||
type Suspension struct {
|
||||
Range Range `json:"range"`
|
||||
Value bool `json:"value"`
|
||||
}
|
||||
|
||||
type Boolean struct {
|
||||
Range Range `json:"range"`
|
||||
Value bool `json:"value"`
|
||||
|
|
@ -1006,6 +1034,21 @@ func (mk *Key) HasTripleGlob() bool {
|
|||
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
|
||||
}
|
||||
|
||||
func (mk *Key) SupportsGlobFilters() bool {
|
||||
if mk.Key.HasGlob() && len(mk.Edges) == 0 {
|
||||
return true
|
||||
|
|
@ -1037,7 +1080,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())
|
||||
}
|
||||
|
|
@ -1326,6 +1384,7 @@ func (ab ArrayNodeBox) Unbox() ArrayNode {
|
|||
// ValueBox is used to box Value for JSON persistence.
|
||||
type ValueBox struct {
|
||||
Null *Null `json:"null,omitempty"`
|
||||
Suspension *Suspension `json:"suspension,omitempty"`
|
||||
Boolean *Boolean `json:"boolean,omitempty"`
|
||||
Number *Number `json:"number,omitempty"`
|
||||
UnquotedString *UnquotedString `json:"unquoted_string,omitempty"`
|
||||
|
|
@ -1341,6 +1400,8 @@ func (vb ValueBox) Unbox() Value {
|
|||
switch {
|
||||
case vb.Null != nil:
|
||||
return vb.Null
|
||||
case vb.Suspension != nil:
|
||||
return vb.Suspension
|
||||
case vb.Boolean != nil:
|
||||
return vb.Boolean
|
||||
case vb.Number != nil:
|
||||
|
|
@ -1369,6 +1430,8 @@ func MakeValueBox(v Value) ValueBox {
|
|||
switch v := v.(type) {
|
||||
case *Null:
|
||||
vb.Null = v
|
||||
case *Suspension:
|
||||
vb.Suspension = v
|
||||
case *Boolean:
|
||||
vb.Boolean = v
|
||||
case *Number:
|
||||
|
|
@ -1394,6 +1457,7 @@ func MakeValueBox(v Value) ValueBox {
|
|||
func (vb ValueBox) ScalarBox() ScalarBox {
|
||||
var sb ScalarBox
|
||||
sb.Null = vb.Null
|
||||
sb.Suspension = vb.Suspension
|
||||
sb.Boolean = vb.Boolean
|
||||
sb.Number = vb.Number
|
||||
sb.UnquotedString = vb.UnquotedString
|
||||
|
|
@ -1416,6 +1480,7 @@ func (vb ValueBox) StringBox() *StringBox {
|
|||
// TODO: implement ScalarString()
|
||||
type ScalarBox struct {
|
||||
Null *Null `json:"null,omitempty"`
|
||||
Suspension *Suspension `json:"suspension,omitempty"`
|
||||
Boolean *Boolean `json:"boolean,omitempty"`
|
||||
Number *Number `json:"number,omitempty"`
|
||||
UnquotedString *UnquotedString `json:"unquoted_string,omitempty"`
|
||||
|
|
@ -1428,6 +1493,8 @@ func (sb ScalarBox) Unbox() Scalar {
|
|||
switch {
|
||||
case sb.Null != nil:
|
||||
return sb.Null
|
||||
case sb.Suspension != nil:
|
||||
return sb.Suspension
|
||||
case sb.Boolean != nil:
|
||||
return sb.Boolean
|
||||
case sb.Number != nil:
|
||||
|
|
@ -1516,7 +1583,7 @@ func RawString(s string, inKey bool) String {
|
|||
return &SingleQuotedString{Value: s}
|
||||
}
|
||||
}
|
||||
} else if s == "null" || strings.ContainsAny(s, UnquotedValueSpecials) {
|
||||
} else if s == "null" || s == "suspend" || s == "unsuspend" || strings.ContainsAny(s, UnquotedValueSpecials) {
|
||||
if !strings.ContainsRune(s, '"') && !strings.ContainsRune(s, '$') {
|
||||
return FlatDoubleQuotedString(s)
|
||||
}
|
||||
|
|
@ -1550,9 +1617,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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ var ReservedKeywords map[string]struct{}
|
|||
// Non Style/Holder keywords.
|
||||
var SimpleReservedKeywords = map[string]struct{}{
|
||||
"label": {},
|
||||
"desc": {},
|
||||
"shape": {},
|
||||
"icon": {},
|
||||
"constraint": {},
|
||||
|
|
@ -31,17 +30,17 @@ var SimpleReservedKeywords = map[string]struct{}{
|
|||
|
||||
// ReservedKeywordHolders are reserved keywords that are meaningless on its own and must hold composites
|
||||
var ReservedKeywordHolders = map[string]struct{}{
|
||||
"style": {},
|
||||
"source-arrowhead": {},
|
||||
"target-arrowhead": {},
|
||||
"style": {},
|
||||
}
|
||||
|
||||
// CompositeReservedKeywords are reserved keywords that can hold composites
|
||||
var CompositeReservedKeywords = map[string]struct{}{
|
||||
"classes": {},
|
||||
"constraint": {},
|
||||
"label": {},
|
||||
"icon": {},
|
||||
"source-arrowhead": {},
|
||||
"target-arrowhead": {},
|
||||
"classes": {},
|
||||
"constraint": {},
|
||||
"label": {},
|
||||
"icon": {},
|
||||
}
|
||||
|
||||
// StyleKeywords are reserved keywords which cannot exist outside of the "style" keyword
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package d2chaos_test
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime/debug"
|
||||
|
|
@ -90,14 +89,14 @@ func TestD2Chaos(t *testing.T) {
|
|||
|
||||
func test(t *testing.T, textPath, text string) {
|
||||
t.Logf("writing d2 to %v (%d bytes)", textPath, len(text))
|
||||
err := ioutil.WriteFile(textPath, []byte(text), 0644)
|
||||
err := os.WriteFile(textPath, []byte(text), 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
goencText := fmt.Sprintf("%#v", text)
|
||||
t.Logf("writing d2.goenc to %v (%d bytes)", textPath+".goenc", len(goencText))
|
||||
err = ioutil.WriteFile(textPath+".goenc", []byte(goencText), 0644)
|
||||
err = os.WriteFile(textPath+".goenc", []byte(goencText), 0644)
|
||||
if err != nil {
|
||||
t.Fatal(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,8 @@ 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 validate 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 +40,8 @@ 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)
|
||||
%[1]s validate file.d2 - Validates file.d2
|
||||
|
||||
See more docs and the source code at https://oss.terrastruct.com/d2.
|
||||
Hosted icons at https://icons.terrastruct.com.
|
||||
|
|
@ -55,7 +59,7 @@ func layoutCmd(ctx context.Context, ms *xmain.State, ps []d2plugin.Plugin) error
|
|||
}
|
||||
}
|
||||
|
||||
func themesCmd(ctx context.Context, ms *xmain.State) {
|
||||
func themesCmd(_ context.Context, ms *xmain.State) {
|
||||
fmt.Fprintf(ms.Stdout, "Available themes:\n%s", d2themescatalog.CLIString())
|
||||
}
|
||||
|
||||
|
|
|
|||
109
d2cli/main.go
109
d2cli/main.go
|
|
@ -50,7 +50,7 @@ import (
|
|||
func Run(ctx context.Context, ms *xmain.State) (err error) {
|
||||
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).")
|
||||
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 will open on a randomly available local port).")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -103,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 {
|
||||
|
|
@ -119,6 +124,23 @@ 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.")
|
||||
|
||||
omitVersionFlag, err := ms.Opts.Bool("OMIT_VERSION", "omit-version", "", false, "omit D2 version from generated image")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
plugins, err := d2plugin.ListPlugins(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -153,7 +175,11 @@ 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 "validate":
|
||||
return validateCmd(ctx, ms)
|
||||
case "version":
|
||||
if len(ms.Opts.Flags.Args()) > 1 {
|
||||
return xmain.UsageErrorf("version subcommand accepts no arguments")
|
||||
|
|
@ -213,7 +239,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() {
|
||||
|
|
@ -303,6 +334,9 @@ func Run(ctx context.Context, ms *xmain.State) (err error) {
|
|||
ThemeID: themeFlag,
|
||||
DarkThemeID: darkThemeFlag,
|
||||
Scale: scale,
|
||||
NoXMLTag: noXMLTagFlag,
|
||||
Salt: saltFlag,
|
||||
OmitVersion: omitVersionFlag,
|
||||
}
|
||||
|
||||
if *watchFlag {
|
||||
|
|
@ -325,6 +359,7 @@ func Run(ctx context.Context, ms *xmain.State) (err error) {
|
|||
forceAppendix: *forceAppendixFlag,
|
||||
pw: pw,
|
||||
fontFamily: fontFamily,
|
||||
outputFormat: outputFormat,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -349,13 +384,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)
|
||||
|
|
@ -430,7 +465,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 {
|
||||
|
|
@ -495,7 +530,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
|
||||
}
|
||||
|
|
@ -522,7 +557,6 @@ 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, inputPath, diagram)
|
||||
|
|
@ -598,9 +632,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
|
||||
|
|
@ -739,7 +773,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 +819,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 +842,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, inputPath, 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 +856,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, inputPath, 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,15 +869,16 @@ 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, inputPath, 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,
|
||||
|
|
@ -852,8 +887,12 @@ func _render(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opts
|
|||
DarkThemeID: opts.DarkThemeID,
|
||||
ThemeOverrides: opts.ThemeOverrides,
|
||||
DarkThemeOverrides: opts.DarkThemeOverrides,
|
||||
NoXMLTag: opts.NoXMLTag,
|
||||
Salt: opts.Salt,
|
||||
Scale: scale,
|
||||
})
|
||||
OmitVersion: opts.OmitVersion,
|
||||
}
|
||||
svg, err := d2svg.Render(diagram, renderOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -874,12 +913,12 @@ func _render(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opts
|
|||
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
|
||||
|
|
@ -937,7 +976,7 @@ func renderPDF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opt
|
|||
scale = go2.Pointer(1.)
|
||||
}
|
||||
|
||||
svg, err = d2svg.Render(diagram, &d2svg.RenderOpts{
|
||||
renderOpts := &d2svg.RenderOpts{
|
||||
Pad: opts.Pad,
|
||||
Sketch: opts.Sketch,
|
||||
Center: opts.Center,
|
||||
|
|
@ -946,7 +985,9 @@ func renderPDF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opt
|
|||
DarkThemeID: opts.DarkThemeID,
|
||||
ThemeOverrides: opts.ThemeOverrides,
|
||||
DarkThemeOverrides: opts.DarkThemeOverrides,
|
||||
})
|
||||
OmitVersion: opts.OmitVersion,
|
||||
}
|
||||
svg, err = d2svg.Render(diagram, renderOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -964,7 +1005,7 @@ func renderPDF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opt
|
|||
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 {
|
||||
|
|
@ -1043,7 +1084,7 @@ func renderPPTX(ctx context.Context, ms *xmain.State, presentation *pptx.Present
|
|||
|
||||
var err error
|
||||
|
||||
svg, err = d2svg.Render(diagram, &d2svg.RenderOpts{
|
||||
renderOpts := &d2svg.RenderOpts{
|
||||
Pad: opts.Pad,
|
||||
Sketch: opts.Sketch,
|
||||
Center: opts.Center,
|
||||
|
|
@ -1052,7 +1093,9 @@ func renderPPTX(ctx context.Context, ms *xmain.State, presentation *pptx.Present
|
|||
DarkThemeID: opts.DarkThemeID,
|
||||
ThemeOverrides: opts.ThemeOverrides,
|
||||
DarkThemeOverrides: opts.DarkThemeOverrides,
|
||||
})
|
||||
OmitVersion: opts.OmitVersion,
|
||||
}
|
||||
svg, err = d2svg.Render(diagram, renderOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -1071,7 +1114,7 @@ func renderPPTX(ctx context.Context, ms *xmain.State, presentation *pptx.Present
|
|||
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 {
|
||||
|
|
@ -1289,7 +1332,7 @@ func renderPNGsForGIF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plug
|
|||
} else {
|
||||
scale = go2.Pointer(1.)
|
||||
}
|
||||
svg, err = d2svg.Render(diagram, &d2svg.RenderOpts{
|
||||
renderOpts := &d2svg.RenderOpts{
|
||||
Pad: opts.Pad,
|
||||
Sketch: opts.Sketch,
|
||||
Center: opts.Center,
|
||||
|
|
@ -1298,7 +1341,9 @@ func renderPNGsForGIF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plug
|
|||
DarkThemeID: opts.DarkThemeID,
|
||||
ThemeOverrides: opts.ThemeOverrides,
|
||||
DarkThemeOverrides: opts.DarkThemeOverrides,
|
||||
})
|
||||
OmitVersion: opts.OmitVersion,
|
||||
}
|
||||
svg, err = d2svg.Render(diagram, renderOpts)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
|
@ -1317,7 +1362,7 @@ func renderPNGsForGIF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plug
|
|||
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 {
|
||||
|
|
|
|||
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);
|
||||
|
|
|
|||
41
d2cli/validate.go
Normal file
41
d2cli/validate.go
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
package d2cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"oss.terrastruct.com/d2/d2lib"
|
||||
"oss.terrastruct.com/util-go/xdefer"
|
||||
"oss.terrastruct.com/util-go/xmain"
|
||||
)
|
||||
|
||||
func validateCmd(ctx context.Context, ms *xmain.State) (err error) {
|
||||
defer xdefer.Errorf(&err, "")
|
||||
|
||||
ms.Opts = xmain.NewOpts(ms.Env, ms.Opts.Flags.Args()[1:])
|
||||
if len(ms.Opts.Args) == 0 {
|
||||
return xmain.UsageErrorf("input argument required")
|
||||
}
|
||||
|
||||
inputPath := ms.Opts.Args[0]
|
||||
if inputPath != "-" {
|
||||
inputPath = ms.AbsPath(inputPath)
|
||||
}
|
||||
|
||||
input, err := ms.ReadPath(inputPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = d2lib.Parse(ctx, string(input), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if inputPath == "-" {
|
||||
inputPath = "Input"
|
||||
}
|
||||
|
||||
fmt.Printf("Success! [%s] is valid D2.\n", inputPath)
|
||||
return nil
|
||||
}
|
||||
|
|
@ -17,9 +17,9 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/coder/websocket"
|
||||
"github.com/coder/websocket/wsjson"
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"nhooyr.io/websocket"
|
||||
"nhooyr.io/websocket/wsjson"
|
||||
|
||||
"oss.terrastruct.com/util-go/xbrowser"
|
||||
|
||||
|
|
@ -57,6 +57,7 @@ type watcherOpts struct {
|
|||
forceAppendix bool
|
||||
pw png.Playwright
|
||||
fontFamily *d2fonts.FontFamily
|
||||
outputFormat exportExtension
|
||||
}
|
||||
|
||||
type watcher struct {
|
||||
|
|
@ -263,6 +264,12 @@ func (w *watcher) watchLoop(ctx context.Context) error {
|
|||
return errors.New("fsnotify watcher closed")
|
||||
}
|
||||
w.ms.Log.Debug.Printf("received file system event %v", ev)
|
||||
|
||||
if isTemp, reason := isBackupFile(ev.Name); isTemp {
|
||||
w.ms.Log.Debug.Printf("skipping event for %q: detected as %s", w.ms.HumanPath(ev.Name), reason)
|
||||
continue
|
||||
}
|
||||
|
||||
mt, err := w.ensureAddWatch(ctx, ev.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -348,6 +355,11 @@ func (w *watcher) ensureAddWatch(ctx context.Context, path string) (time.Time, e
|
|||
}
|
||||
|
||||
func (w *watcher) addWatch(ctx context.Context, path string) (time.Time, error) {
|
||||
if isTemp, reason := isBackupFile(path); isTemp {
|
||||
w.ms.Log.Debug.Printf("skipping watch for %q: detected as %s", w.ms.HumanPath(path), reason)
|
||||
return time.Time{}, nil
|
||||
}
|
||||
|
||||
err := w.fw.Add(path)
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
|
|
@ -430,7 +442,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 {
|
||||
|
|
@ -670,3 +682,41 @@ func (tfs *trackedFS) Open(name string) (fs.File, error) {
|
|||
}
|
||||
return f, err
|
||||
}
|
||||
|
||||
func isBackupFile(path string) (bool, string) {
|
||||
ext := filepath.Ext(path)
|
||||
baseName := filepath.Base(path)
|
||||
|
||||
// This list is based off of https://github.com/gohugoio/hugo/blob/master/commands/hugobuilder.go#L795
|
||||
switch {
|
||||
case strings.HasSuffix(ext, "~"):
|
||||
return true, "generic backup file (~)"
|
||||
case ext == ".swp":
|
||||
return true, "vim swap file"
|
||||
case ext == ".swx":
|
||||
return true, "vim swap file"
|
||||
case ext == ".tmp":
|
||||
return true, "generic temp file"
|
||||
case ext == ".DS_Store":
|
||||
return true, "OSX thumbnail"
|
||||
case ext == ".bck":
|
||||
return true, "Helix backup"
|
||||
case baseName == "4913":
|
||||
return true, "vim temp file"
|
||||
case strings.HasPrefix(ext, ".goutputstream"):
|
||||
return true, "GNOME temp file"
|
||||
case strings.HasSuffix(ext, "jb_old___"):
|
||||
return true, "IntelliJ old backup"
|
||||
case strings.HasSuffix(ext, "jb_tmp___"):
|
||||
return true, "IntelliJ temp file"
|
||||
case strings.HasSuffix(ext, "jb_bak___"):
|
||||
return true, "IntelliJ backup"
|
||||
case strings.HasPrefix(ext, ".sb-"):
|
||||
return true, "Byword temp file"
|
||||
case strings.HasPrefix(baseName, ".#"):
|
||||
return true, "Emacs lock file"
|
||||
case strings.HasPrefix(baseName, "#"):
|
||||
return true, "Emacs temp file"
|
||||
}
|
||||
return false, ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"io"
|
||||
"io/fs"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
|
|
@ -19,6 +20,7 @@ import (
|
|||
"oss.terrastruct.com/d2/d2parser"
|
||||
"oss.terrastruct.com/d2/d2target"
|
||||
"oss.terrastruct.com/d2/lib/color"
|
||||
"oss.terrastruct.com/d2/lib/geo"
|
||||
"oss.terrastruct.com/d2/lib/textmeasure"
|
||||
)
|
||||
|
||||
|
|
@ -86,6 +88,7 @@ func (c *compiler) compileBoard(g *d2graph.Graph, ir *d2ir.Map) *d2graph.Graph {
|
|||
ir = ir.Copy(nil).(*d2ir.Map)
|
||||
// c.preprocessSeqDiagrams(ir)
|
||||
c.compileMap(g.Root, ir)
|
||||
c.setDefaultShapes(g)
|
||||
if len(c.err.Errors) == 0 {
|
||||
c.validateKeys(g.Root, ir)
|
||||
}
|
||||
|
|
@ -94,6 +97,8 @@ func (c *compiler) compileBoard(g *d2graph.Graph, ir *d2ir.Map) *d2graph.Graph {
|
|||
c.validateEdges(g)
|
||||
c.validatePositionsCompatibility(g)
|
||||
|
||||
c.compileLegend(g, ir)
|
||||
|
||||
c.compileBoardsField(g, ir, "layers")
|
||||
c.compileBoardsField(g, ir, "scenarios")
|
||||
c.compileBoardsField(g, ir, "steps")
|
||||
|
|
@ -108,8 +113,55 @@ func (c *compiler) compileBoard(g *d2graph.Graph, ir *d2ir.Map) *d2graph.Graph {
|
|||
return g
|
||||
}
|
||||
|
||||
func (c *compiler) compileLegend(g *d2graph.Graph, m *d2ir.Map) {
|
||||
varsField := m.GetField(d2ast.FlatUnquotedString("vars"))
|
||||
if varsField == nil || varsField.Map() == nil {
|
||||
return
|
||||
}
|
||||
|
||||
legendField := varsField.Map().GetField(d2ast.FlatUnquotedString("d2-legend"))
|
||||
if legendField == nil || legendField.Map() == nil {
|
||||
return
|
||||
}
|
||||
|
||||
legendGraph := d2graph.NewGraph()
|
||||
|
||||
c.compileMap(legendGraph.Root, legendField.Map())
|
||||
c.setDefaultShapes(legendGraph)
|
||||
|
||||
objects := make([]*d2graph.Object, 0)
|
||||
for _, obj := range legendGraph.Objects {
|
||||
if obj.Style.Opacity != nil {
|
||||
if opacity, err := strconv.ParseFloat(obj.Style.Opacity.Value, 64); err == nil && opacity == 0 {
|
||||
continue
|
||||
}
|
||||
}
|
||||
obj.Box = &geo.Box{}
|
||||
obj.TopLeft = geo.NewPoint(10, 10)
|
||||
obj.Width = 100
|
||||
obj.Height = 100
|
||||
objects = append(objects, obj)
|
||||
}
|
||||
|
||||
for _, edge := range legendGraph.Edges {
|
||||
edge.Route = []*geo.Point{
|
||||
{X: 10, Y: 10},
|
||||
{X: 110, Y: 10},
|
||||
}
|
||||
}
|
||||
|
||||
legend := &d2graph.Legend{
|
||||
Objects: objects,
|
||||
Edges: legendGraph.Edges,
|
||||
}
|
||||
|
||||
if len(legend.Objects) > 0 || len(legend.Edges) > 0 {
|
||||
g.Legend = legend
|
||||
}
|
||||
}
|
||||
|
||||
func (c *compiler) compileBoardsField(g *d2graph.Graph, ir *d2ir.Map, fieldName string) {
|
||||
boards := ir.GetField(fieldName)
|
||||
boards := ir.GetField(d2ast.FlatUnquotedString(fieldName))
|
||||
if boards.Map() == nil {
|
||||
return
|
||||
}
|
||||
|
|
@ -118,8 +170,8 @@ func (c *compiler) compileBoardsField(g *d2graph.Graph, ir *d2ir.Map, fieldName
|
|||
if f.Map() == nil {
|
||||
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()
|
||||
|
|
@ -129,7 +181,10 @@ func (c *compiler) compileBoardsField(g *d2graph.Graph, ir *d2ir.Map, fieldName
|
|||
g2.BaseAST = findFieldAST(g.BaseAST, f)
|
||||
}
|
||||
c.compileBoard(g2, m)
|
||||
g2.Name = f.Name
|
||||
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)
|
||||
|
|
@ -145,7 +200,7 @@ func findFieldAST(ast *d2ast.Map, f *d2ir.Field) *d2ast.Map {
|
|||
path := []string{}
|
||||
curr := f
|
||||
for {
|
||||
path = append([]string{curr.Name}, path...)
|
||||
path = append([]string{curr.Name.ScalarString()}, path...)
|
||||
boardKind := d2ir.NodeBoardKind(curr)
|
||||
if boardKind == "" {
|
||||
break
|
||||
|
|
@ -153,55 +208,48 @@ func findFieldAST(ast *d2ast.Map, f *d2ir.Field) *d2ast.Map {
|
|||
curr = d2ir.ParentField(curr)
|
||||
}
|
||||
|
||||
currAST := ast
|
||||
for len(path) > 0 {
|
||||
head := path[0]
|
||||
found := false
|
||||
for _, n := range currAST.Nodes {
|
||||
if n.MapKey == nil {
|
||||
continue
|
||||
}
|
||||
if n.MapKey.Key == nil {
|
||||
continue
|
||||
}
|
||||
if len(n.MapKey.Key.Path) != 1 {
|
||||
continue
|
||||
}
|
||||
head2 := n.MapKey.Key.Path[0].Unbox().ScalarString()
|
||||
if head == head2 {
|
||||
currAST = n.MapKey.Value.Map
|
||||
// The BaseAST is only used for making edits to the AST (through d2oracle)
|
||||
// If there's no Map for a given board, either it's an empty layer or set to an import
|
||||
// Either way, in order to make edits, it needs to be expanded into a Map to add lines to
|
||||
if currAST == nil {
|
||||
n.MapKey.Value.Map = &d2ast.Map{
|
||||
Range: d2ast.MakeRange(",1:0:0-1:0:0"),
|
||||
}
|
||||
if n.MapKey.Value.Import != nil {
|
||||
imp := &d2ast.Import{
|
||||
Range: d2ast.MakeRange(",1:0:0-1:0:0"),
|
||||
Spread: true,
|
||||
Pre: n.MapKey.Value.Import.Pre,
|
||||
Path: n.MapKey.Value.Import.Path,
|
||||
}
|
||||
n.MapKey.Value.Map.Nodes = append(n.MapKey.Value.Map.Nodes, d2ast.MapNodeBox{
|
||||
Import: imp,
|
||||
})
|
||||
return _findFieldAST(ast, path)
|
||||
}
|
||||
|
||||
}
|
||||
currAST = n.MapKey.Value.Map
|
||||
}
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return nil
|
||||
}
|
||||
path = path[1:]
|
||||
func _findFieldAST(ast *d2ast.Map, path []string) *d2ast.Map {
|
||||
if len(path) == 0 {
|
||||
return ast
|
||||
}
|
||||
|
||||
return currAST
|
||||
head := path[0]
|
||||
remainingPath := path[1:]
|
||||
|
||||
for i := range ast.Nodes {
|
||||
if ast.Nodes[i].MapKey == nil || ast.Nodes[i].MapKey.Key == nil || len(ast.Nodes[i].MapKey.Key.Path) != 1 {
|
||||
continue
|
||||
}
|
||||
|
||||
head2 := ast.Nodes[i].MapKey.Key.Path[0].Unbox().ScalarString()
|
||||
if head == head2 {
|
||||
if ast.Nodes[i].MapKey.Value.Map == nil {
|
||||
ast.Nodes[i].MapKey.Value.Map = &d2ast.Map{
|
||||
Range: d2ast.MakeRange(",1:0:0-1:0:0"),
|
||||
}
|
||||
if ast.Nodes[i].MapKey.Value.Import != nil {
|
||||
imp := &d2ast.Import{
|
||||
Range: d2ast.MakeRange(",1:0:0-1:0:0"),
|
||||
Spread: true,
|
||||
Pre: ast.Nodes[i].MapKey.Value.Import.Pre,
|
||||
Path: ast.Nodes[i].MapKey.Value.Import.Path,
|
||||
}
|
||||
ast.Nodes[i].MapKey.Value.Map.Nodes = append(ast.Nodes[i].MapKey.Value.Map.Nodes, d2ast.MapNodeBox{
|
||||
Import: imp,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if result := _findFieldAST(ast.Nodes[i].MapKey.Value.Map, remainingPath); result != nil {
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type compiler struct {
|
||||
|
|
@ -220,7 +268,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 {
|
||||
|
|
@ -235,8 +283,6 @@ func (c *compiler) compileMap(obj *d2graph.Object, m *d2ir.Map) {
|
|||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
c.errorf(class.LastRef().AST(), "class missing value")
|
||||
}
|
||||
|
||||
for _, className := range classNames {
|
||||
|
|
@ -261,7 +307,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")
|
||||
|
|
@ -270,10 +316,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 := d2ast.BoardKeywords[f.Name]; ok {
|
||||
if _, ok := d2ast.BoardKeywords[f.Name.ScalarString()]; ok && f.Name.IsUnquoted() {
|
||||
continue
|
||||
}
|
||||
c.compileField(obj, f)
|
||||
|
|
@ -294,14 +340,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)
|
||||
keyword := strings.ToLower(f.Name.ScalarString())
|
||||
_, isStyleReserved := d2ast.StyleKeywords[keyword]
|
||||
if isStyleReserved {
|
||||
c.errorf(f.LastRef().AST(), "%v must be style.%v", f.Name, f.Name)
|
||||
if isStyleReserved && f.Name.IsUnquoted() {
|
||||
c.errorf(f.LastRef().AST(), "%v must be style.%v", f.Name.ScalarString(), f.Name.ScalarString())
|
||||
return
|
||||
}
|
||||
_, isReserved := d2ast.SimpleReservedKeywords[keyword]
|
||||
if f.Name == "classes" {
|
||||
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")
|
||||
|
|
@ -311,34 +358,31 @@ func (c *compiler) compileField(obj *d2graph.Object, f *d2ir.Field) {
|
|||
continue
|
||||
}
|
||||
for _, cf := range classesField.Map().Fields {
|
||||
if _, ok := d2ast.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`)
|
||||
}
|
||||
c.compileStyle(&obj.Attributes.Style, f.Map())
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -354,7 +398,7 @@ func (c *compiler) compileField(obj *d2graph.Object, f *d2ir.Field) {
|
|||
}
|
||||
|
||||
parent := obj
|
||||
obj = obj.EnsureChild(d2graphIDA([]string{f.Name}))
|
||||
obj = obj.EnsureChild(([]d2ast.String{f.Name}))
|
||||
if f.Primary() != nil {
|
||||
c.compileLabel(&obj.Attributes, f)
|
||||
}
|
||||
|
|
@ -383,7 +427,7 @@ func (c *compiler) compileField(obj *d2graph.Object, f *d2ir.Field) {
|
|||
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)
|
||||
|
|
@ -403,8 +447,6 @@ func (c *compiler) compileLabel(attrs *d2graph.Attributes, f d2ir.Node) {
|
|||
attrs.Language = fullTag
|
||||
}
|
||||
switch attrs.Language {
|
||||
case "latex":
|
||||
attrs.Shape.Value = d2target.ShapeText
|
||||
case "markdown":
|
||||
rendered, err := textmeasure.RenderMarkdown(scalar.ScalarString())
|
||||
if err != nil {
|
||||
|
|
@ -421,9 +463,6 @@ func (c *compiler) compileLabel(attrs *d2graph.Attributes, f d2ir.Node) {
|
|||
c.errorf(f.LastPrimaryKey(), "malformed Markdown: %s", err.Error())
|
||||
}
|
||||
}
|
||||
attrs.Shape.Value = d2target.ShapeText
|
||||
default:
|
||||
attrs.Shape.Value = d2target.ShapeCode
|
||||
}
|
||||
attrs.Label.Value = scalar.ScalarString()
|
||||
default:
|
||||
|
|
@ -436,7 +475,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 {
|
||||
|
|
@ -448,7 +487,7 @@ func (c *compiler) compilePosition(attrs *d2graph.Attributes, f *d2ir.Field) {
|
|||
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()
|
||||
|
|
@ -463,7 +502,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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -476,7 +515,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 {
|
||||
|
|
@ -501,24 +540,27 @@ 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)
|
||||
case "shape":
|
||||
in := d2target.IsShape(scalar.ScalarString())
|
||||
_, isArrowhead := d2target.Arrowheads[scalar.ScalarString()]
|
||||
shapeVal := strings.ToLower(scalar.ScalarString())
|
||||
in := d2target.IsShape(shapeVal)
|
||||
_, isArrowhead := d2target.Arrowheads[shapeVal]
|
||||
if !in && !isArrowhead {
|
||||
c.errorf(scalar, "unknown shape %q", scalar.ScalarString())
|
||||
return
|
||||
}
|
||||
attrs.Shape.Value = scalar.ScalarString()
|
||||
attrs.Shape.Value = shapeVal
|
||||
if strings.EqualFold(attrs.Shape.Value, d2target.ShapeCode) {
|
||||
// Explicit code shape is plaintext.
|
||||
attrs.Language = d2target.ShapeText
|
||||
|
|
@ -532,6 +574,17 @@ func (c *compiler) compileReserved(attrs *d2graph.Attributes, f *d2ir.Field) {
|
|||
}
|
||||
attrs.Icon = iconURL
|
||||
c.compilePosition(attrs, f)
|
||||
if f.Map() != nil {
|
||||
for _, ff := range f.Map().Fields {
|
||||
if ff.Name.ScalarString() == "style" && ff.Name.IsUnquoted() {
|
||||
if ff.Map() == nil || len(ff.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(&attrs.IconStyle, ff.Map())
|
||||
}
|
||||
}
|
||||
}
|
||||
case "near":
|
||||
nearKey, err := d2parser.ParseKey(scalar.ScalarString())
|
||||
if err != nil {
|
||||
|
|
@ -594,11 +647,12 @@ func (c *compiler) compileReserved(attrs *d2graph.Attributes, f *d2ir.Field) {
|
|||
attrs.Link.MapKey = f.LastPrimaryKey()
|
||||
case "direction":
|
||||
dirs := []string{"up", "down", "right", "left"}
|
||||
if !go2.Contains(dirs, scalar.ScalarString()) {
|
||||
val := strings.ToLower(scalar.ScalarString())
|
||||
if !go2.Contains(dirs, val) {
|
||||
c.errorf(scalar, `direction must be one of %v, got %q`, strings.Join(dirs, ", "), scalar.ScalarString())
|
||||
return
|
||||
}
|
||||
attrs.Direction.Value = scalar.ScalarString()
|
||||
attrs.Direction.Value = val
|
||||
attrs.Direction.MapKey = f.LastPrimaryKey()
|
||||
case "constraint":
|
||||
if _, ok := scalar.(d2ast.String); !ok {
|
||||
|
|
@ -676,6 +730,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 != "" {
|
||||
|
|
@ -684,84 +745,76 @@ func (c *compiler) compileReserved(attrs *d2graph.Attributes, f *d2ir.Field) {
|
|||
}
|
||||
}
|
||||
|
||||
func (c *compiler) compileStyle(attrs *d2graph.Attributes, m *d2ir.Map) {
|
||||
func (c *compiler) compileStyle(styles *d2graph.Style, m *d2ir.Map) {
|
||||
for _, f := range m.Fields {
|
||||
c.compileStyleField(attrs, f)
|
||||
c.compileStyleField(styles, f)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *compiler) compileStyleField(attrs *d2graph.Attributes, f *d2ir.Field) {
|
||||
if _, ok := d2ast.StyleKeywords[strings.ToLower(f.Name)]; !ok {
|
||||
c.errorf(f.LastRef().AST(), `invalid style keyword: "%s"`, f.Name)
|
||||
func (c *compiler) compileStyleField(styles *d2graph.Style, f *d2ir.Field) {
|
||||
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 {
|
||||
return
|
||||
}
|
||||
compileStyleFieldInit(attrs, f)
|
||||
compileStyleFieldInit(styles, f)
|
||||
scalar := f.Primary().Value
|
||||
err := attrs.Style.Apply(f.Name, scalar.ScalarString())
|
||||
err := styles.Apply(f.Name.ScalarString(), scalar.ScalarString())
|
||||
if err != nil {
|
||||
c.errorf(scalar, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func compileStyleFieldInit(attrs *d2graph.Attributes, f *d2ir.Field) {
|
||||
switch f.Name {
|
||||
func compileStyleFieldInit(styles *d2graph.Style, f *d2ir.Field) {
|
||||
switch f.Name.ScalarString() {
|
||||
case "opacity":
|
||||
attrs.Style.Opacity = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
||||
styles.Opacity = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
||||
case "stroke":
|
||||
attrs.Style.Stroke = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
||||
styles.Stroke = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
||||
case "fill":
|
||||
attrs.Style.Fill = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
||||
styles.Fill = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
||||
case "fill-pattern":
|
||||
attrs.Style.FillPattern = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
||||
styles.FillPattern = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
||||
case "stroke-width":
|
||||
attrs.Style.StrokeWidth = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
||||
styles.StrokeWidth = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
||||
case "stroke-dash":
|
||||
attrs.Style.StrokeDash = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
||||
styles.StrokeDash = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
||||
case "border-radius":
|
||||
attrs.Style.BorderRadius = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
||||
styles.BorderRadius = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
||||
case "shadow":
|
||||
attrs.Style.Shadow = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
||||
styles.Shadow = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
||||
case "3d":
|
||||
attrs.Style.ThreeDee = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
||||
styles.ThreeDee = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
||||
case "multiple":
|
||||
attrs.Style.Multiple = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
||||
styles.Multiple = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
||||
case "font":
|
||||
attrs.Style.Font = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
||||
styles.Font = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
||||
case "font-size":
|
||||
attrs.Style.FontSize = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
||||
styles.FontSize = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
||||
case "font-color":
|
||||
attrs.Style.FontColor = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
||||
styles.FontColor = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
||||
case "animated":
|
||||
attrs.Style.Animated = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
||||
styles.Animated = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
||||
case "bold":
|
||||
attrs.Style.Bold = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
||||
styles.Bold = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
||||
case "italic":
|
||||
attrs.Style.Italic = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
||||
styles.Italic = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
||||
case "underline":
|
||||
attrs.Style.Underline = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
||||
styles.Underline = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
||||
case "filled":
|
||||
attrs.Style.Filled = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
||||
case "width":
|
||||
attrs.WidthAttr = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
||||
case "height":
|
||||
attrs.HeightAttr = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
||||
case "top":
|
||||
attrs.Top = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
||||
case "left":
|
||||
attrs.Left = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
||||
styles.Filled = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
||||
case "double-border":
|
||||
attrs.Style.DoubleBorder = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
||||
styles.DoubleBorder = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
||||
case "text-transform":
|
||||
attrs.Style.TextTransform = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
||||
styles.TextTransform = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
|
@ -785,7 +838,7 @@ func (c *compiler) compileEdge(obj *d2graph.Object, e *d2ir.Edge) {
|
|||
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)
|
||||
|
|
@ -793,7 +846,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 {
|
||||
|
|
@ -808,8 +861,6 @@ func (c *compiler) compileEdgeMap(edge *d2graph.Edge, m *d2ir.Map) {
|
|||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
c.errorf(class.LastRef().AST(), "class missing value")
|
||||
}
|
||||
|
||||
for _, className := range classNames {
|
||||
|
|
@ -820,8 +871,8 @@ func (c *compiler) compileEdgeMap(edge *d2graph.Edge, m *d2ir.Map) {
|
|||
}
|
||||
}
|
||||
for _, f := range m.Fields {
|
||||
_, ok := d2ast.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
|
||||
}
|
||||
|
|
@ -830,32 +881,33 @@ 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)
|
||||
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 := 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
|
||||
}
|
||||
c.compileStyle(&edge.Attributes, f.Map())
|
||||
c.compileStyle(&edge.Attributes.Style, f.Map())
|
||||
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{}
|
||||
}
|
||||
|
|
@ -873,16 +925,17 @@ 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)
|
||||
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
|
||||
}
|
||||
c.compileStyle(attrs, f2.Map())
|
||||
c.compileStyle(&attrs.Style, f2.Map())
|
||||
continue
|
||||
} else {
|
||||
c.errorf(f2.LastRef().AST(), `source-arrowhead/target-arrowhead map keys must be reserved keywords`)
|
||||
|
|
@ -991,7 +1044,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 := d2ast.BoardKeywords[f.Name]; ok {
|
||||
if _, ok := d2ast.BoardKeywords[f.Name.ScalarString()]; ok && f.Name.IsUnquoted() {
|
||||
continue
|
||||
}
|
||||
c.validateKey(obj, f)
|
||||
|
|
@ -999,8 +1052,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)
|
||||
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:
|
||||
|
|
@ -1010,7 +1064,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) {
|
||||
|
|
@ -1045,7 +1099,7 @@ func (c *compiler) validateKey(obj *d2graph.Object, f *d2ir.Field) {
|
|||
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())
|
||||
}
|
||||
|
|
@ -1207,7 +1261,7 @@ func (c *compiler) validateBoardLinks(g *d2graph.Graph) {
|
|||
}
|
||||
|
||||
u, err := url.Parse(html.UnescapeString(obj.Link.Value))
|
||||
isRemote := err == nil && strings.HasPrefix(u.Scheme, "http")
|
||||
isRemote := err == nil && (u.Scheme != "" || strings.HasPrefix(u.Path, "/"))
|
||||
if isRemote {
|
||||
continue
|
||||
}
|
||||
|
|
@ -1221,6 +1275,11 @@ func (c *compiler) validateBoardLinks(g *d2graph.Graph) {
|
|||
obj.Link = nil
|
||||
continue
|
||||
}
|
||||
|
||||
if slices.Equal(linkKey.StringIDA(), obj.Graph.IDA()) {
|
||||
obj.Link = nil
|
||||
continue
|
||||
}
|
||||
}
|
||||
for _, b := range g.Layers {
|
||||
c.validateBoardLinks(b)
|
||||
|
|
@ -1233,34 +1292,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:])
|
||||
}
|
||||
}
|
||||
|
|
@ -1275,20 +1334,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
|
||||
}
|
||||
|
|
@ -1340,8 +1389,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
|
||||
}
|
||||
|
|
@ -1356,7 +1405,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())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1401,7 +1450,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
|
||||
}
|
||||
}
|
||||
|
|
@ -1410,7 +1459,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
|
||||
}
|
||||
|
|
@ -1419,36 +1468,42 @@ 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("center"))
|
||||
if f != nil {
|
||||
val, _ := strconv.ParseBool(f.Primary().Value.ScalarString())
|
||||
config.Center = &val
|
||||
}
|
||||
|
||||
f = configMap.GetField(d2ast.FlatUnquotedString("theme-overrides"))
|
||||
if f != nil {
|
||||
overrides, err := compileThemeOverrides(f.Map())
|
||||
if err != nil {
|
||||
|
|
@ -1456,7 +1511,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 {
|
||||
|
|
@ -1464,12 +1519,12 @@ func compileConfig(ir *d2ir.Map) (*d2target.Config, error) {
|
|||
}
|
||||
config.DarkThemeOverrides = overrides
|
||||
}
|
||||
f = configMap.GetField("data")
|
||||
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] = f.Primary().Value.ScalarString()
|
||||
config.Data[f.Name.ScalarString()] = f.Primary().Value.ScalarString()
|
||||
} else if f.Composite != nil {
|
||||
var arr []interface{}
|
||||
switch c := f.Composite.(type) {
|
||||
|
|
@ -1481,7 +1536,7 @@ func compileConfig(ir *d2ir.Map) (*d2target.Config, error) {
|
|||
}
|
||||
}
|
||||
}
|
||||
config.Data[f.Name] = arr
|
||||
config.Data[f.Name.ScalarString()] = arr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1498,7 +1553,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":
|
||||
|
|
@ -1536,11 +1591,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))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1553,3 +1608,21 @@ FOR:
|
|||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (c *compiler) setDefaultShapes(g *d2graph.Graph) {
|
||||
for _, obj := range g.Objects {
|
||||
if obj.Shape.Value == "" {
|
||||
if obj.OuterSequenceDiagram() != nil {
|
||||
obj.Shape.Value = d2target.ShapeRectangle
|
||||
} else if obj.Language == "latex" {
|
||||
obj.Shape.Value = d2target.ShapeText
|
||||
} else if obj.Language == "markdown" {
|
||||
obj.Shape.Value = d2target.ShapeText
|
||||
} else if obj.Language != "" {
|
||||
obj.Shape.Value = d2target.ShapeCode
|
||||
} else {
|
||||
obj.Shape.Value = d2target.ShapeRectangle
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -8,6 +8,7 @@ import (
|
|||
|
||||
"oss.terrastruct.com/util-go/go2"
|
||||
|
||||
"oss.terrastruct.com/d2/d2ast"
|
||||
"oss.terrastruct.com/d2/d2graph"
|
||||
"oss.terrastruct.com/d2/d2parser"
|
||||
"oss.terrastruct.com/d2/d2renderers/d2fonts"
|
||||
|
|
@ -15,6 +16,7 @@ import (
|
|||
"oss.terrastruct.com/d2/d2themes"
|
||||
"oss.terrastruct.com/d2/lib/color"
|
||||
"oss.terrastruct.com/d2/lib/geo"
|
||||
"oss.terrastruct.com/d2/lib/label"
|
||||
)
|
||||
|
||||
func Export(ctx context.Context, g *d2graph.Graph, fontFamily *d2fonts.FontFamily) (*d2target.Diagram, error) {
|
||||
|
|
@ -45,6 +47,26 @@ func Export(ctx context.Context, g *d2graph.Graph, fontFamily *d2fonts.FontFamil
|
|||
diagram.Connections[i] = toConnection(g.Edges[i], g.Theme)
|
||||
}
|
||||
|
||||
if g.Legend != nil {
|
||||
legend := &d2target.Legend{}
|
||||
|
||||
if len(g.Legend.Objects) > 0 {
|
||||
legend.Shapes = make([]d2target.Shape, len(g.Legend.Objects))
|
||||
for i, obj := range g.Legend.Objects {
|
||||
legend.Shapes[i] = toShape(obj, g)
|
||||
}
|
||||
}
|
||||
|
||||
if len(g.Legend.Edges) > 0 {
|
||||
legend.Connections = make([]d2target.Connection, len(g.Legend.Edges))
|
||||
for i, edge := range g.Legend.Edges {
|
||||
legend.Connections[i] = toConnection(edge, g.Theme)
|
||||
}
|
||||
}
|
||||
|
||||
diagram.Legend = legend
|
||||
}
|
||||
|
||||
return diagram, nil
|
||||
}
|
||||
|
||||
|
|
@ -77,6 +99,46 @@ func applyTheme(shape *d2target.Shape, obj *d2graph.Object, theme *d2themes.Them
|
|||
if theme.SpecialRules.Mono {
|
||||
shape.FontFamily = "mono"
|
||||
}
|
||||
if theme.SpecialRules.C4 && len(obj.ChildrenArray) > 0 {
|
||||
if obj.Style.Fill == nil {
|
||||
shape.Fill = "transparent"
|
||||
}
|
||||
if obj.Style.Stroke == nil {
|
||||
shape.Stroke = color.AA2
|
||||
}
|
||||
if obj.Style.StrokeDash == nil {
|
||||
shape.StrokeDash = 5
|
||||
}
|
||||
if obj.Style.FontColor == nil {
|
||||
shape.Color = color.N1
|
||||
}
|
||||
}
|
||||
if theme.SpecialRules.C4 && obj.Level() == 1 && len(obj.ChildrenArray) == 0 &&
|
||||
obj.Shape.Value != d2target.ShapePerson && obj.Shape.Value != d2target.ShapeC4Person {
|
||||
if obj.Style.Fill == nil {
|
||||
shape.Fill = color.B6
|
||||
}
|
||||
if obj.Style.Stroke == nil {
|
||||
shape.Stroke = color.B5
|
||||
}
|
||||
}
|
||||
if theme.SpecialRules.C4 && (obj.Shape.Value == d2target.ShapePerson || obj.Shape.Value == d2target.ShapeC4Person) {
|
||||
if obj.Style.Fill == nil {
|
||||
shape.Fill = color.B2
|
||||
}
|
||||
if obj.Style.Stroke == nil {
|
||||
shape.Stroke = color.B1
|
||||
}
|
||||
}
|
||||
if theme.SpecialRules.C4 && obj.Level() > 1 && len(obj.ChildrenArray) == 0 &&
|
||||
obj.Shape.Value != d2target.ShapePerson && obj.Shape.Value != d2target.ShapeC4Person {
|
||||
if obj.Style.Fill == nil {
|
||||
shape.Fill = color.B4
|
||||
}
|
||||
if obj.Style.Stroke == nil {
|
||||
shape.Stroke = color.B3
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -132,6 +194,9 @@ func applyStyles(shape *d2target.Shape, obj *d2graph.Object) {
|
|||
if obj.Style.DoubleBorder != nil {
|
||||
shape.DoubleBorder, _ = strconv.ParseBool(obj.Style.DoubleBorder.Value)
|
||||
}
|
||||
if obj.IconStyle.BorderRadius != nil {
|
||||
shape.IconBorderRadius, _ = strconv.Atoi(obj.IconStyle.BorderRadius.Value)
|
||||
}
|
||||
}
|
||||
|
||||
func toShape(obj *d2graph.Object, g *d2graph.Graph) d2target.Shape {
|
||||
|
|
@ -144,6 +209,7 @@ func toShape(obj *d2graph.Object, g *d2graph.Graph) d2target.Shape {
|
|||
shape.Pos = d2target.NewPoint(int(obj.TopLeft.X), int(obj.TopLeft.Y))
|
||||
shape.Width = int(obj.Width)
|
||||
shape.Height = int(obj.Height)
|
||||
shape.Language = obj.Language
|
||||
|
||||
text := obj.Text()
|
||||
shape.Bold = text.IsBold
|
||||
|
|
@ -162,12 +228,18 @@ func toShape(obj *d2graph.Object, g *d2graph.Graph) d2target.Shape {
|
|||
applyStyles(shape, obj)
|
||||
applyTheme(shape, obj, g.Theme)
|
||||
shape.Color = text.GetColor(shape.Italic)
|
||||
if g.Theme != nil && g.Theme.SpecialRules.C4 {
|
||||
if obj.Style.FontColor == nil {
|
||||
if len(obj.ChildrenArray) > 0 {
|
||||
shape.Color = color.N1
|
||||
} else {
|
||||
shape.Color = color.N7
|
||||
}
|
||||
}
|
||||
}
|
||||
applyStyles(shape, obj)
|
||||
|
||||
switch obj.Shape.Value {
|
||||
case d2target.ShapeCode, d2target.ShapeText:
|
||||
shape.Language = obj.Language
|
||||
shape.Label = obj.Label.Value
|
||||
switch strings.ToLower(obj.Shape.Value) {
|
||||
case d2target.ShapeClass:
|
||||
shape.Class = *obj.Class
|
||||
// The label is the header for classes and tables, which is set in client to be 4 px larger than the object's set font size
|
||||
|
|
@ -194,6 +266,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)
|
||||
|
|
@ -332,7 +407,18 @@ func toConnection(edge *d2graph.Edge, theme *d2themes.Theme) d2target.Connection
|
|||
if edge.Tooltip != nil {
|
||||
connection.Tooltip = edge.Tooltip.Value
|
||||
}
|
||||
connection.Icon = edge.Icon
|
||||
if edge.Icon != nil {
|
||||
connection.Icon = edge.Icon
|
||||
if edge.IconPosition != nil {
|
||||
connection.IconPosition = (d2ast.LabelPositionsMapping[edge.IconPosition.Value]).String()
|
||||
} else {
|
||||
connection.IconPosition = label.InsideMiddleCenter.String()
|
||||
}
|
||||
}
|
||||
|
||||
if edge.IconStyle.BorderRadius != nil {
|
||||
connection.IconBorderRadius, _ = strconv.ParseFloat(edge.IconStyle.BorderRadius.Value, 64)
|
||||
}
|
||||
|
||||
if edge.Style.Italic != nil {
|
||||
connection.Italic, _ = strconv.ParseBool(edge.Style.Italic.Value)
|
||||
|
|
@ -354,6 +440,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
|
||||
|
|
@ -377,5 +466,17 @@ func toConnection(edge *d2graph.Edge, theme *d2themes.Theme) d2target.Connection
|
|||
connection.Src = edge.Src.AbsID()
|
||||
connection.Dst = edge.Dst.AbsID()
|
||||
|
||||
if theme != nil && theme.SpecialRules.C4 {
|
||||
if edge.Style.StrokeDash == nil {
|
||||
connection.StrokeDash = 5
|
||||
}
|
||||
if edge.Style.Stroke == nil {
|
||||
connection.Stroke = color.AA4
|
||||
}
|
||||
if edge.Style.FontColor == nil {
|
||||
connection.Color = color.N2
|
||||
}
|
||||
}
|
||||
|
||||
return *connection
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ func TestExport(t *testing.T) {
|
|||
t.Run("connection", testConnection)
|
||||
t.Run("label", testLabel)
|
||||
t.Run("theme", testTheme)
|
||||
t.Run("legend", testLegend)
|
||||
}
|
||||
|
||||
func testShape(t *testing.T) {
|
||||
|
|
@ -204,6 +205,30 @@ func testTheme(t *testing.T) {
|
|||
runa(t, tcs)
|
||||
}
|
||||
|
||||
func testLegend(t *testing.T) {
|
||||
tcs := []testCase{
|
||||
{
|
||||
name: "basic_legend",
|
||||
dsl: `vars: {
|
||||
d2-legend: {
|
||||
legend: {
|
||||
l1: Rectangles {shape: rectangle}
|
||||
l2: Ovals {shape: oval}
|
||||
l1 -> l2: Connection
|
||||
}
|
||||
}
|
||||
}
|
||||
x: {shape: rectangle}
|
||||
y: {shape: oval}
|
||||
x -> y: connects
|
||||
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
runa(t, tcs)
|
||||
}
|
||||
|
||||
func runa(t *testing.T, tcs []testCase) {
|
||||
for _, tc := range tcs {
|
||||
tc := tc
|
||||
|
|
@ -303,10 +328,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)
|
||||
|
|
|
|||
|
|
@ -42,6 +42,12 @@ func (p *printer) node(n d2ast.Node) {
|
|||
p.blockComment(n)
|
||||
case *d2ast.Null:
|
||||
p.sb.WriteString("null")
|
||||
case *d2ast.Suspension:
|
||||
if n.Value {
|
||||
p.sb.WriteString("suspend")
|
||||
} else {
|
||||
p.sb.WriteString("unsuspend")
|
||||
}
|
||||
case *d2ast.Boolean:
|
||||
p.sb.WriteString(strconv.FormatBool(n.Value))
|
||||
case *d2ast.Number:
|
||||
|
|
@ -121,7 +127,7 @@ func (p *printer) blockComment(bc *d2ast.BlockComment) {
|
|||
}
|
||||
|
||||
func (p *printer) interpolationBoxes(boxes []d2ast.InterpolationBox, isDoubleString bool) {
|
||||
for _, b := range boxes {
|
||||
for i, b := range boxes {
|
||||
if b.Substitution != nil {
|
||||
p.substitution(b.Substitution)
|
||||
continue
|
||||
|
|
@ -134,6 +140,11 @@ func (p *printer) interpolationBoxes(boxes []d2ast.InterpolationBox, isDoubleStr
|
|||
s = escapeUnquotedValue(*b.String, p.inKey)
|
||||
}
|
||||
b.StringRaw = &s
|
||||
} else if i > 0 && boxes[i-1].Substitution != nil {
|
||||
// If this string follows a substitution, we need to make sure to use
|
||||
// the actual string content, not the raw value which might be incorrect
|
||||
s := *b.String
|
||||
b.StringRaw = &s
|
||||
}
|
||||
if !isDoubleString {
|
||||
if _, ok := d2ast.ReservedKeywords[strings.ToLower(*b.StringRaw)]; ok {
|
||||
|
|
@ -293,11 +304,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
|
||||
|
|
|
|||
|
|
@ -881,6 +881,30 @@ coop: {
|
|||
fill: blue
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "remove-empty-boards",
|
||||
in: `k
|
||||
|
||||
layers
|
||||
scenarios: {}
|
||||
steps: asdf
|
||||
`,
|
||||
exp: `k
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "vars",
|
||||
in: `vars: {
|
||||
a: "a"
|
||||
b: "X${a})"
|
||||
}
|
||||
`,
|
||||
exp: `vars: {
|
||||
a: "a"
|
||||
b: "X${a})"
|
||||
}
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ type Graph struct {
|
|||
BaseAST *d2ast.Map `json:"-"`
|
||||
|
||||
Root *Object `json:"root"`
|
||||
Legend *Legend `json:"legend,omitempty"`
|
||||
Edges []*Edge `json:"edges"`
|
||||
Objects []*Object `json:"objects"`
|
||||
|
||||
|
|
@ -67,6 +68,11 @@ type Graph struct {
|
|||
Data map[string]interface{} `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
type Legend struct {
|
||||
Objects []*Object `json:"objects,omitempty"`
|
||||
Edges []*Edge `json:"edges,omitempty"`
|
||||
}
|
||||
|
||||
func NewGraph() *Graph {
|
||||
d := &Graph{}
|
||||
d.Root = &Object{
|
||||
|
|
@ -84,6 +90,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
|
||||
|
||||
|
|
@ -130,10 +196,11 @@ type Attributes struct {
|
|||
Label Scalar `json:"label"`
|
||||
LabelDimensions d2target.TextDimensions `json:"labelDimensions"`
|
||||
|
||||
Style Style `json:"style"`
|
||||
Icon *url.URL `json:"icon,omitempty"`
|
||||
Tooltip *Scalar `json:"tooltip,omitempty"`
|
||||
Link *Scalar `json:"link,omitempty"`
|
||||
Style Style `json:"style"`
|
||||
Icon *url.URL `json:"icon,omitempty"`
|
||||
IconStyle Style `json:"iconStyle"`
|
||||
Tooltip *Scalar `json:"tooltip,omitempty"`
|
||||
Link *Scalar `json:"link,omitempty"`
|
||||
|
||||
WidthAttr *Scalar `json:"width,omitempty"`
|
||||
HeightAttr *Scalar `json:"height,omitempty"`
|
||||
|
|
@ -504,7 +571,7 @@ func (obj *Object) GetFill() string {
|
|||
return color.AB5
|
||||
}
|
||||
|
||||
if strings.EqualFold(shape, d2target.ShapePerson) {
|
||||
if strings.EqualFold(shape, d2target.ShapePerson) || strings.EqualFold(shape, d2target.ShapeC4Person) {
|
||||
return color.B3
|
||||
}
|
||||
if strings.EqualFold(shape, d2target.ShapeDiamond) {
|
||||
|
|
@ -634,7 +701,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 {
|
||||
|
|
@ -647,9 +717,6 @@ func (obj *Object) newObject(id string) *Object {
|
|||
Label: Scalar{
|
||||
Value: idval,
|
||||
},
|
||||
Shape: Scalar{
|
||||
Value: d2target.ShapeRectangle,
|
||||
},
|
||||
},
|
||||
|
||||
Graph: obj.Graph,
|
||||
|
|
@ -787,7 +854,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:
|
||||
|
|
@ -803,12 +870,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.
|
||||
|
|
@ -824,9 +891,11 @@ func (obj *Object) EnsureChild(ida []string) *Object {
|
|||
return obj
|
||||
}
|
||||
|
||||
_, is := d2ast.ReservedKeywordHolders[ida[0]]
|
||||
_, is := d2ast.ReservedKeywordHolders[ida[0].ScalarString()]
|
||||
is = is && ida[0].IsUnquoted()
|
||||
if len(ida) == 1 && !is {
|
||||
_, ok := d2ast.ReservedKeywords[ida[0]]
|
||||
_, ok := d2ast.ReservedKeywords[ida[0].ScalarString()]
|
||||
ok = ok && ida[0].IsUnquoted()
|
||||
if ok {
|
||||
return obj
|
||||
}
|
||||
|
|
@ -835,11 +904,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)
|
||||
}
|
||||
|
|
@ -877,14 +949,16 @@ func (obj *Object) GetLabelSize(mtexts []*d2target.MText, ruler *textmeasure.Rul
|
|||
|
||||
var dims *d2target.TextDimensions
|
||||
switch shapeType {
|
||||
case d2target.ShapeText:
|
||||
case d2target.ShapeClass:
|
||||
dims = GetTextDimensions(mtexts, ruler, obj.Text(), go2.Pointer(d2fonts.SourceCodePro))
|
||||
default:
|
||||
if obj.Language == "latex" {
|
||||
width, height, err := d2latex.Measure(obj.Text().Text)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dims = d2target.NewTextDimensions(width, height)
|
||||
} else if obj.Language != "" {
|
||||
} else if obj.Language != "" && shapeType != d2target.ShapeCode {
|
||||
var err error
|
||||
dims, err = getMarkdownDimensions(mtexts, ruler, obj.Text(), fontFamily)
|
||||
if err != nil {
|
||||
|
|
@ -893,12 +967,6 @@ func (obj *Object) GetLabelSize(mtexts []*d2target.MText, ruler *textmeasure.Rul
|
|||
} else {
|
||||
dims = GetTextDimensions(mtexts, ruler, obj.Text(), fontFamily)
|
||||
}
|
||||
|
||||
case d2target.ShapeClass:
|
||||
dims = GetTextDimensions(mtexts, ruler, obj.Text(), go2.Pointer(d2fonts.SourceCodePro))
|
||||
|
||||
default:
|
||||
dims = GetTextDimensions(mtexts, ruler, obj.Text(), fontFamily)
|
||||
}
|
||||
|
||||
if shapeType == d2target.ShapeSQLTable && obj.Label.Value == "" {
|
||||
|
|
@ -1238,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 := d2ast.ReservedKeywords[p]; ok {
|
||||
if _, ok := d2ast.ReservedKeywords[p.ScalarString()]; ok && p.IsUnquoted() {
|
||||
return nil, errors.New("cannot connect to reserved keyword")
|
||||
}
|
||||
}
|
||||
|
|
@ -1269,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.
|
||||
|
|
@ -1279,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
|
||||
|
|
@ -1293,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
|
||||
|
|
@ -1544,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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
558
d2ir/compile.go
558
d2ir/compile.go
|
|
@ -15,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 {
|
||||
|
|
@ -80,6 +81,7 @@ func Compile(ast *d2ast.Map, opts *CompileOptions) (*Map, []string, error) {
|
|||
c.compileMap(m, ast, ast)
|
||||
c.compileSubstitutions(m, nil)
|
||||
c.overlayClasses(m)
|
||||
m.removeSuspendedFields()
|
||||
if !c.err.Empty() {
|
||||
return nil, nil, c.err
|
||||
}
|
||||
|
|
@ -87,12 +89,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
|
||||
}
|
||||
|
|
@ -103,16 +105,15 @@ 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)
|
||||
l.Fields = append(l.Fields, lClasses)
|
||||
} else {
|
||||
} else if lClasses.Map() != nil {
|
||||
base := classes.Copy(l).(*Field)
|
||||
OverlayMap(base.Map(), lClasses.Map())
|
||||
l.DeleteField("classes")
|
||||
|
|
@ -125,7 +126,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...)
|
||||
}
|
||||
}
|
||||
|
|
@ -147,9 +151,9 @@ func (c *compiler) compileSubstitutions(m *Map, varsStack []*Map) {
|
|||
}
|
||||
}
|
||||
} else if f.Map() != nil {
|
||||
if f.Name == "vars" {
|
||||
if f.Name != nil && f.Name.ScalarString() == "vars" && f.Name.IsUnquoted() {
|
||||
c.compileSubstitutions(f.Map(), varsStack)
|
||||
c.validateConfigs(f.Map().GetField("d2-config"))
|
||||
c.validateConfigs(f.Map().GetField(d2ast.FlatUnquotedString("d2-config")))
|
||||
} else {
|
||||
c.compileSubstitutions(f.Map(), varsStack)
|
||||
}
|
||||
|
|
@ -171,37 +175,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" && f.Name != "data" {
|
||||
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", "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{}) {
|
||||
|
|
@ -211,12 +215,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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -276,6 +280,19 @@ func (c *compiler) resolveSubstitutions(varsStack []*Map, node Node) (removedFie
|
|||
break
|
||||
}
|
||||
}
|
||||
|
||||
if removedField && len(m.globs) > 0 && !c.lazyGlobBeingApplied {
|
||||
origGlobStack := c.globContextStack
|
||||
c.globContextStack = append(c.globContextStack, m.globs)
|
||||
for _, gctx := range m.globs {
|
||||
old := c.lazyGlobBeingApplied
|
||||
c.lazyGlobBeingApplied = true
|
||||
c.compileKey(gctx.refctx)
|
||||
c.lazyGlobBeingApplied = old
|
||||
}
|
||||
c.globContextStack = origGlobStack
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
if resolvedField.Primary() == nil {
|
||||
|
|
@ -342,10 +359,37 @@ 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) 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 {
|
||||
nestedVars := make(map[string]string)
|
||||
c.collectVariables(f.Map(), nestedVars)
|
||||
for k, v := range nestedVars {
|
||||
variables[f.Name.ScalarString()+"."+k] = v
|
||||
}
|
||||
c.collectVariables(f.Map(), variables)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *compiler) resolveSubstitution(vars *Map, node Node, substitution *d2ast.Substitution, isCurrentScopeVars bool) *Field {
|
||||
if vars == nil {
|
||||
return nil
|
||||
|
|
@ -355,7 +399,7 @@ func (c *compiler) resolveSubstitution(vars *Map, node Node, substitution *d2ast
|
|||
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
|
||||
}
|
||||
|
|
@ -375,7 +419,7 @@ func (c *compiler) resolveSubstitution(vars *Map, node Node, substitution *d2ast
|
|||
//
|
||||
// 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 == p.Unbox().ScalarString() && isCurrentScopeVars && parent.Name == "vars" {
|
||||
if fok && fieldNode.Name != nil && fieldNode.Name.ScalarString() == p.Unbox().ScalarString() && isCurrentScopeVars && parent.Name.ScalarString() == "vars" && parent.Name.IsUnquoted() {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -417,22 +461,6 @@ func (g *globContext) copyApplied(from *globContext) {
|
|||
}
|
||||
}
|
||||
|
||||
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...)
|
||||
}
|
||||
if len(prefix.Path) > 0 {
|
||||
g2.refctx.Key.Key = prefix
|
||||
}
|
||||
if !g2.refctx.Key.HasTripleGlob() && g2.refctx.Key.EdgeKey != nil {
|
||||
prefix.Path = append(prefix.Path, g2.refctx.Key.EdgeKey.Path...)
|
||||
}
|
||||
return g2
|
||||
}
|
||||
|
||||
func (c *compiler) ampersandFilterMap(dst *Map, ast, scopeAST *d2ast.Map) bool {
|
||||
for _, n := range ast.Nodes {
|
||||
switch {
|
||||
|
|
@ -456,10 +484,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)
|
||||
|
|
@ -479,16 +507,28 @@ 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) == BoardScenario {
|
||||
for _, g := range previousGlobs {
|
||||
g2 := 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.HasTripleGlob() {
|
||||
if !g.refctx.Key.HasMultiGlob() {
|
||||
// Triple globs already apply independently to each board
|
||||
g2.copyApplied(g)
|
||||
}
|
||||
|
|
@ -496,7 +536,9 @@ func (c *compiler) compileMap(dst *Map, ast, scopeAST *d2ast.Map) {
|
|||
}
|
||||
} else if NodeBoardKind(dst) == BoardStep {
|
||||
for _, g := range previousGlobs {
|
||||
globs = append(globs, g.prefixed(dst))
|
||||
gctx2 := g.copy()
|
||||
gctx2.refctx.ScopeMap = dst
|
||||
globs = append(globs, gctx2)
|
||||
}
|
||||
} else {
|
||||
globs = append(globs, previousGlobs...)
|
||||
|
|
@ -562,6 +604,20 @@ func (c *compiler) compileMap(dst *Map, ast, scopeAST *d2ast.Map) {
|
|||
c.ensureGlobContext(gctx2.refctx)
|
||||
}
|
||||
|
||||
scenariosField := impn.Map().GetField(d2ast.FlatUnquotedString("scenarios"))
|
||||
if scenariosField != nil && scenariosField.Map() != nil {
|
||||
for _, sf := range scenariosField.Map().Fields {
|
||||
c.overlay(dst, sf)
|
||||
}
|
||||
}
|
||||
|
||||
stepsField := impn.Map().GetField(d2ast.FlatUnquotedString("steps"))
|
||||
if stepsField != nil && stepsField.Map() != nil {
|
||||
for _, sf := range stepsField.Map().Fields {
|
||||
c.overlay(dst, sf)
|
||||
}
|
||||
}
|
||||
|
||||
OverlayMap(dst, impn.Map())
|
||||
impDir := n.Import.Dir()
|
||||
c.extendLinks(dst, ParentField(dst), impDir)
|
||||
|
|
@ -670,13 +726,77 @@ func (c *compiler) ampersandFilter(refctx *RefContext) bool {
|
|||
return true
|
||||
}
|
||||
|
||||
keyPath := refctx.Key.Key
|
||||
if keyPath == nil || len(keyPath.Path) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
firstPart := keyPath.Path[0].Unbox().ScalarString()
|
||||
if (firstPart == "src" || firstPart == "dst") && len(keyPath.Path) > 1 {
|
||||
if len(c.mapRefContextStack) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
edge := ParentEdge(refctx.ScopeMap)
|
||||
if edge == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
var nodePath []d2ast.String
|
||||
if firstPart == "src" {
|
||||
nodePath = edge.ID.SrcPath
|
||||
} else {
|
||||
nodePath = edge.ID.DstPath
|
||||
}
|
||||
|
||||
rootMap := RootMap(refctx.ScopeMap)
|
||||
node := rootMap.GetField(nodePath...)
|
||||
if node == nil || node.Map() == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
secondPart := keyPath.Path[1].Unbox().ScalarString()
|
||||
value := refctx.Key.Value.ScalarBox().Unbox().ScalarString()
|
||||
|
||||
if len(keyPath.Path) == 2 && c._ampersandPropertyFilter(secondPart, value, node, refctx.Key) {
|
||||
return true
|
||||
}
|
||||
|
||||
propKeyPath := &d2ast.KeyPath{
|
||||
Path: keyPath.Path[1:],
|
||||
}
|
||||
|
||||
propKey := &d2ast.Key{
|
||||
Key: propKeyPath,
|
||||
Value: refctx.Key.Value,
|
||||
}
|
||||
|
||||
propRefCtx := &RefContext{
|
||||
Key: propKey,
|
||||
ScopeMap: node.Map(),
|
||||
ScopeAST: refctx.ScopeAST,
|
||||
}
|
||||
|
||||
fa, err := node.Map().EnsureField(propKeyPath, propRefCtx, false, c)
|
||||
if err != nil || len(fa) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, f := range fa {
|
||||
if c._ampersandFilter(f, propRefCtx) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fa, err := refctx.ScopeMap.EnsureField(refctx.Key.Key, refctx, false, c)
|
||||
if err != nil {
|
||||
c.err.Errors = append(c.err.Errors, err.(d2ast.Error))
|
||||
return false
|
||||
}
|
||||
if len(fa) == 0 {
|
||||
if refctx.Key.Value.ScalarBox().Unbox().ScalarString() == "*" {
|
||||
if refctx.Key.Value.ScalarBox().Unbox() != nil && refctx.Key.Value.ScalarBox().Unbox().ScalarString() == "*" {
|
||||
return false
|
||||
}
|
||||
// The field/edge has no value for this filter
|
||||
|
|
@ -727,6 +847,7 @@ func (c *compiler) ampersandFilter(refctx *RefContext) bool {
|
|||
},
|
||||
}
|
||||
return c._ampersandFilter(f, refctx)
|
||||
|
||||
case "label":
|
||||
f := &Field{}
|
||||
n := refctx.ScopeMap.Parent()
|
||||
|
|
@ -735,7 +856,7 @@ func (c *compiler) ampersandFilter(refctx *RefContext) bool {
|
|||
case *Field:
|
||||
// The label value for fields is their key value
|
||||
f.Primary_ = &Scalar{
|
||||
Value: d2ast.FlatUnquotedString(n.Name),
|
||||
Value: n.Name,
|
||||
}
|
||||
case *Edge:
|
||||
// But for edges, it's nothing
|
||||
|
|
@ -745,8 +866,83 @@ func (c *compiler) ampersandFilter(refctx *RefContext) bool {
|
|||
f.Primary_ = n.Primary()
|
||||
}
|
||||
return c._ampersandFilter(f, refctx)
|
||||
case "src":
|
||||
if len(c.mapRefContextStack) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
edge := ParentEdge(refctx.ScopeMap)
|
||||
if edge == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
filterValue := refctx.Key.Value.ScalarBox().Unbox().ScalarString()
|
||||
|
||||
var srcParts []string
|
||||
for _, part := range edge.ID.SrcPath {
|
||||
srcParts = append(srcParts, part.ScalarString())
|
||||
}
|
||||
|
||||
container := ParentField(edge)
|
||||
if container != nil && container.Name.ScalarString() != "root" {
|
||||
containerPath := []string{}
|
||||
curr := container
|
||||
for curr != nil && curr.Name.ScalarString() != "root" {
|
||||
containerPath = append([]string{curr.Name.ScalarString()}, containerPath...)
|
||||
curr = ParentField(curr)
|
||||
}
|
||||
|
||||
srcStart := srcParts[0]
|
||||
if !strings.EqualFold(srcStart, containerPath[0]) {
|
||||
srcParts = append(containerPath, srcParts...)
|
||||
}
|
||||
}
|
||||
|
||||
srcPath := strings.Join(srcParts, ".")
|
||||
|
||||
return srcPath == filterValue
|
||||
|
||||
case "dst":
|
||||
if len(c.mapRefContextStack) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
edge := ParentEdge(refctx.ScopeMap)
|
||||
if edge == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
filterValue := refctx.Key.Value.ScalarBox().Unbox().ScalarString()
|
||||
|
||||
var dstParts []string
|
||||
for _, part := range edge.ID.DstPath {
|
||||
dstParts = append(dstParts, part.ScalarString())
|
||||
}
|
||||
|
||||
// Find the container that holds this edge
|
||||
// Build the absolute path by prepending the container's path
|
||||
container := ParentField(edge)
|
||||
if container != nil && container.Name.ScalarString() != "root" {
|
||||
containerPath := []string{}
|
||||
curr := container
|
||||
for curr != nil && curr.Name.ScalarString() != "root" {
|
||||
containerPath = append([]string{curr.Name.ScalarString()}, containerPath...)
|
||||
curr = ParentField(curr)
|
||||
}
|
||||
|
||||
dstStart := dstParts[0]
|
||||
if !strings.EqualFold(dstStart, containerPath[0]) {
|
||||
dstParts = append(containerPath, dstParts...)
|
||||
}
|
||||
}
|
||||
dstPath := strings.Join(dstParts, ".")
|
||||
|
||||
return dstPath == filterValue
|
||||
default:
|
||||
return false
|
||||
f := refctx.ScopeMap.Parent().(*Field)
|
||||
propName := refctx.Key.Key.Last().ScalarString()
|
||||
value := refctx.Key.Value.ScalarBox().Unbox().ScalarString()
|
||||
return c._ampersandPropertyFilter(propName, value, f, refctx.Key)
|
||||
}
|
||||
}
|
||||
for _, f := range fa {
|
||||
|
|
@ -758,6 +954,70 @@ func (c *compiler) ampersandFilter(refctx *RefContext) bool {
|
|||
return true
|
||||
}
|
||||
|
||||
// handles filters that are not based on fields
|
||||
func (c *compiler) _ampersandPropertyFilter(propName string, value string, node *Field, key *d2ast.Key) bool {
|
||||
switch propName {
|
||||
case "level":
|
||||
levelVal, err := strconv.Atoi(value)
|
||||
if err != nil {
|
||||
c.errorf(key, `&level must be a non-negative integer, got %q`, value)
|
||||
return false
|
||||
}
|
||||
if levelVal < 0 {
|
||||
c.errorf(key, `&level must be a non-negative integer, got %d`, levelVal)
|
||||
return false
|
||||
}
|
||||
|
||||
level := 0
|
||||
parent := ParentField(node)
|
||||
for parent != nil && parent.Name.ScalarString() != "root" && NodeBoardKind(parent) == "" {
|
||||
level++
|
||||
parent = ParentField(parent)
|
||||
}
|
||||
return level == levelVal
|
||||
case "leaf":
|
||||
boolVal, err := strconv.ParseBool(value)
|
||||
if err != nil {
|
||||
c.errorf(key, `&leaf must be "true" or "false", got %q`, value)
|
||||
return false
|
||||
}
|
||||
isLeaf := node.Map() == nil || !c.IsContainer(node.Map())
|
||||
return isLeaf == boolVal
|
||||
case "connected":
|
||||
boolVal, err := strconv.ParseBool(value)
|
||||
if err != nil {
|
||||
c.errorf(key, `&connected must be "true" or "false", got %q`, value)
|
||||
return false
|
||||
}
|
||||
isConnected := false
|
||||
for _, r := range node.References {
|
||||
if r.InEdge() {
|
||||
isConnected = true
|
||||
break
|
||||
}
|
||||
}
|
||||
return isConnected == boolVal
|
||||
case "label":
|
||||
f := &Field{}
|
||||
if node.Primary() == nil {
|
||||
f.Primary_ = &Scalar{
|
||||
Value: node.Name,
|
||||
}
|
||||
} else {
|
||||
f.Primary_ = node.Primary()
|
||||
}
|
||||
propKey := &d2ast.Key{
|
||||
Key: key.Key,
|
||||
Value: key.Value,
|
||||
}
|
||||
propRefCtx := &RefContext{
|
||||
Key: propKey,
|
||||
}
|
||||
return c._ampersandFilter(f, propRefCtx)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *compiler) _ampersandFilter(f *Field, refctx *RefContext) bool {
|
||||
if refctx.Key.Value.ScalarBox().Unbox() == nil {
|
||||
c.errorf(refctx.Key, "glob filters cannot be composites")
|
||||
|
|
@ -811,11 +1071,22 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
if len(refctx.Key.Edges) == 0 && (refctx.Key.Primary.Suspension != nil || refctx.Key.Value.Suspension != nil) {
|
||||
if !c.lazyGlobBeingApplied {
|
||||
if refctx.Key.Primary.Suspension != nil {
|
||||
f.suspended = refctx.Key.Primary.Suspension.Value
|
||||
} else {
|
||||
f.suspended = refctx.Key.Value.Suspension.Value
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if refctx.Key.Primary.Unbox() != nil {
|
||||
if c.ignoreLazyGlob(f) {
|
||||
return
|
||||
|
|
@ -875,6 +1146,11 @@ func (c *compiler) _compileField(f *Field, refctx *RefContext) {
|
|||
return
|
||||
}
|
||||
n.(Importable).SetImportAST(refctx.Key.Value.Import)
|
||||
var existingEdges []*Edge
|
||||
if f.Map() != nil {
|
||||
existingEdges = f.Map().Edges
|
||||
}
|
||||
originalF := f.Copy(refctx.ScopeMap).(*Field)
|
||||
switch n := n.(type) {
|
||||
case *Field:
|
||||
if n.Primary_ != nil {
|
||||
|
|
@ -911,6 +1187,22 @@ func (c *compiler) _compileField(f *Field, refctx *RefContext) {
|
|||
c.overlayClasses(f.Map())
|
||||
}
|
||||
}
|
||||
OverlayField(f, originalF)
|
||||
if existingEdges != nil && f.Map() != nil {
|
||||
for _, edge := range existingEdges {
|
||||
exists := false
|
||||
for _, currentEdge := range f.Map().Edges {
|
||||
if currentEdge.ID.Match(edge.ID) {
|
||||
exists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !exists {
|
||||
f.Map().Edges = append(f.Map().Edges, edge)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} else if refctx.Key.Value.ScalarBox().Unbox() != nil {
|
||||
if c.ignoreLazyGlob(f) {
|
||||
return
|
||||
|
|
@ -920,7 +1212,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)
|
||||
}
|
||||
}
|
||||
|
|
@ -943,7 +1235,11 @@ 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" {
|
||||
// A substitute or such
|
||||
if f.Name == nil {
|
||||
continue
|
||||
}
|
||||
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
|
||||
|
|
@ -951,7 +1247,7 @@ func (c *compiler) extendLinks(m *Map, importF *Field, importDir string) {
|
|||
val := f.Primary().Value.ScalarString()
|
||||
|
||||
u, err := url.Parse(html.UnescapeString(val))
|
||||
isRemote := err == nil && strings.HasPrefix(u.Scheme, "http")
|
||||
isRemote := err == nil && (u.Scheme != "" || strings.HasPrefix(u.Path, "/"))
|
||||
if isRemote {
|
||||
continue
|
||||
}
|
||||
|
|
@ -966,11 +1262,11 @@ func (c *compiler) extendLinks(m *Map, importF *Field, importDir string) {
|
|||
}
|
||||
|
||||
for _, id := range linkIDA[1:] {
|
||||
if id == "_" {
|
||||
if id.ScalarString() == "_" && id.IsUnquoted() {
|
||||
if len(linkIDA) < 2 || len(importIDA) < 2 {
|
||||
break
|
||||
}
|
||||
linkIDA = append([]string{linkIDA[0]}, linkIDA[2:]...)
|
||||
linkIDA = append([]d2ast.String{linkIDA[0]}, linkIDA[2:]...)
|
||||
importIDA = importIDA[:len(importIDA)-2]
|
||||
} else {
|
||||
break
|
||||
|
|
@ -978,14 +1274,18 @@ func (c *compiler) extendLinks(m *Map, importF *Field, importDir string) {
|
|||
}
|
||||
|
||||
extendedIDA := append(importIDA, linkIDA[1:]...)
|
||||
kp := d2ast.MakeKeyPath(extendedIDA)
|
||||
kp := d2ast.MakeKeyPathString(extendedIDA)
|
||||
s := d2format.Format(kp)
|
||||
f.Primary_.Value = d2ast.MakeValueBox(d2ast.FlatUnquotedString(s)).ScalarBox().Unbox()
|
||||
}
|
||||
if f.Name == "icon" && f.Primary() != nil {
|
||||
if f.Name.ScalarString() == "icon" && f.Name.IsUnquoted() && f.Primary() != nil {
|
||||
val := f.Primary().Value.ScalarString()
|
||||
// It's likely a substitution
|
||||
if val == "" {
|
||||
continue
|
||||
}
|
||||
u, err := url.Parse(html.UnescapeString(val))
|
||||
isRemoteImg := err == nil && strings.HasPrefix(u.Scheme, "http")
|
||||
isRemoteImg := err == nil && (u.Scheme != "" || strings.HasPrefix(u.Path, "/"))
|
||||
if isRemoteImg {
|
||||
continue
|
||||
}
|
||||
|
|
@ -1016,30 +1316,29 @@ func (c *compiler) compileLink(f *Field, refctx *RefContext) {
|
|||
return
|
||||
}
|
||||
|
||||
if linkIDA[0] == "root" {
|
||||
c.errorf(refctx.Key.Key, "cannot refer to root in link")
|
||||
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 {
|
||||
// Leave the underscore. It will fail in compiler as a standalone board,
|
||||
// but if imported, will get further resolved in extendLinks
|
||||
|
|
@ -1050,12 +1349,12 @@ func (c *compiler) compileLink(f *Field, refctx *RefContext) {
|
|||
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))
|
||||
}
|
||||
|
||||
|
|
@ -1111,6 +1410,99 @@ func (c *compiler) _compileEdges(refctx *RefContext) {
|
|||
refctx.ScopeMap.DeleteEdge(e.ID)
|
||||
continue
|
||||
}
|
||||
|
||||
if refctx.Key.Value.Map != nil && refctx.Key.Value.Map.HasFilter() {
|
||||
if e.Map_ == nil {
|
||||
e.Map_ = &Map{
|
||||
parent: e,
|
||||
}
|
||||
}
|
||||
c.mapRefContextStack = append(c.mapRefContextStack, refctx)
|
||||
ok := c.ampersandFilterMap(e.Map_, refctx.Key.Value.Map, refctx.ScopeAST)
|
||||
c.mapRefContextStack = c.mapRefContextStack[:len(c.mapRefContextStack)-1]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if refctx.Key.Primary.Suspension != nil || refctx.Key.Value.Suspension != nil {
|
||||
if !c.lazyGlobBeingApplied {
|
||||
// Check if edge passes filter before applying suspension
|
||||
if refctx.Key.Value.Map != nil && refctx.Key.Value.Map.HasFilter() {
|
||||
if e.Map_ == nil {
|
||||
e.Map_ = &Map{
|
||||
parent: e,
|
||||
}
|
||||
}
|
||||
c.mapRefContextStack = append(c.mapRefContextStack, refctx)
|
||||
ok := c.ampersandFilterMap(e.Map_, refctx.Key.Value.Map, refctx.ScopeAST)
|
||||
c.mapRefContextStack = c.mapRefContextStack[:len(c.mapRefContextStack)-1]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
var suspensionValue bool
|
||||
if refctx.Key.Primary.Suspension != nil {
|
||||
suspensionValue = refctx.Key.Primary.Suspension.Value
|
||||
} else {
|
||||
suspensionValue = refctx.Key.Value.Suspension.Value
|
||||
}
|
||||
e.suspended = suspensionValue
|
||||
|
||||
// If we're unsuspending an edge, we should also unsuspend its src and dst objects
|
||||
// And their ancestors
|
||||
if !suspensionValue {
|
||||
srcPath, dstPath := e.ID.SrcPath, e.ID.DstPath
|
||||
|
||||
// Make paths absolute if they're relative
|
||||
container := ParentField(e)
|
||||
if container != nil && container.Name.ScalarString() != "root" {
|
||||
containerPath := []d2ast.String{}
|
||||
curr := container
|
||||
for curr != nil && curr.Name.ScalarString() != "root" {
|
||||
containerPath = append([]d2ast.String{curr.Name}, containerPath...)
|
||||
curr = ParentField(curr)
|
||||
}
|
||||
|
||||
if len(srcPath) > 0 && !strings.EqualFold(srcPath[0].ScalarString(), containerPath[0].ScalarString()) {
|
||||
absSrcPath := append([]d2ast.String{}, containerPath...)
|
||||
srcPath = append(absSrcPath, srcPath...)
|
||||
}
|
||||
|
||||
if len(dstPath) > 0 && !strings.EqualFold(dstPath[0].ScalarString(), containerPath[0].ScalarString()) {
|
||||
absDstPath := append([]d2ast.String{}, containerPath...)
|
||||
dstPath = append(absDstPath, dstPath...)
|
||||
}
|
||||
}
|
||||
|
||||
rootMap := RootMap(refctx.ScopeMap)
|
||||
srcObj := rootMap.GetField(srcPath...)
|
||||
dstObj := rootMap.GetField(dstPath...)
|
||||
|
||||
// Unsuspend source node and all its ancestors
|
||||
if srcObj != nil {
|
||||
srcObj.suspended = false
|
||||
parent := ParentField(srcObj)
|
||||
for parent != nil && parent.Name.ScalarString() != "root" {
|
||||
parent.suspended = false
|
||||
parent = ParentField(parent)
|
||||
}
|
||||
}
|
||||
|
||||
// Unsuspend destination node and all its ancestors
|
||||
if dstObj != nil {
|
||||
dstObj.suspended = false
|
||||
parent := ParentField(dstObj)
|
||||
for parent != nil && parent.Name.ScalarString() != "root" {
|
||||
parent.suspended = false
|
||||
parent = ParentField(parent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
e.References = append(e.References, &EdgeReference{
|
||||
Context_: refctx,
|
||||
DueToGlob_: len(c.globRefContextStack) > 0,
|
||||
|
|
@ -1137,7 +1529,7 @@ func (c *compiler) _compileEdges(refctx *RefContext) {
|
|||
}
|
||||
c.compileField(e.Map_, refctx.Key.EdgeKey, refctx)
|
||||
} else {
|
||||
if refctx.Key.Primary.Unbox() != nil {
|
||||
if refctx.Key.Primary.Unbox() != nil && refctx.Key.Primary.Suspension == nil {
|
||||
if c.ignoreLazyGlob(e) {
|
||||
return
|
||||
}
|
||||
|
|
@ -1158,7 +1550,7 @@ func (c *compiler) _compileEdges(refctx *RefContext) {
|
|||
c.mapRefContextStack = append(c.mapRefContextStack, refctx)
|
||||
c.compileMap(e.Map_, refctx.Key.Value.Map, refctx.ScopeAST)
|
||||
c.mapRefContextStack = c.mapRefContextStack[:len(c.mapRefContextStack)-1]
|
||||
} else if refctx.Key.Value.ScalarBox().Unbox() != nil {
|
||||
} else if refctx.Key.Value.ScalarBox().Unbox() != nil && refctx.Key.Value.Suspension == nil {
|
||||
if c.ignoreLazyGlob(e) {
|
||||
return
|
||||
}
|
||||
|
|
@ -1229,8 +1621,46 @@ func (c *compiler) compileArray(dst *Array, a *d2ast.Array, scopeAST *d2ast.Map)
|
|||
Value: []d2ast.InterpolationBox{{Substitution: an.Substitution}},
|
||||
},
|
||||
}
|
||||
case *d2ast.Comment:
|
||||
continue
|
||||
}
|
||||
|
||||
dst.Values = append(dst.Values, irv)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Map) removeSuspendedFields() {
|
||||
if m == nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, f := range m.Fields {
|
||||
if f.Map() != nil {
|
||||
f.Map().removeSuspendedFields()
|
||||
}
|
||||
}
|
||||
|
||||
for i := len(m.Fields) - 1; i >= 0; i-- {
|
||||
if m.Fields[i].Name == nil {
|
||||
continue
|
||||
}
|
||||
_, isReserved := d2ast.ReservedKeywords[m.Fields[i].Name.ScalarString()]
|
||||
if isReserved {
|
||||
continue
|
||||
}
|
||||
if m.Fields[i].suspended {
|
||||
m.DeleteField(m.Fields[i].Name.ScalarString())
|
||||
}
|
||||
}
|
||||
|
||||
for _, e := range m.Edges {
|
||||
if e.Map() != nil {
|
||||
e.Map().removeSuspendedFields()
|
||||
}
|
||||
}
|
||||
for i := len(m.Edges) - 1; i >= 0; i-- {
|
||||
if m.Edges[i].suspended {
|
||||
m.DeleteEdge(m.Edges[i].ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -195,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) {
|
||||
|
|
@ -576,7 +593,7 @@ classes: {
|
|||
}
|
||||
}
|
||||
`)
|
||||
assert.ErrorString(t, err, `TestCompile/classes/nonroot.d2:2:3: classes is only allowed at a board root`)
|
||||
assert.ErrorString(t, err, `TestCompile/classes/nonroot.d2:2:3: classes must be declared at a board root scope`)
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -722,7 +739,7 @@ dora: {
|
|||
}
|
||||
`)
|
||||
assert.Success(t, err)
|
||||
assert.Equal(t, "grid-columns", m.Fields[1].Map().Fields[0].Name)
|
||||
assert.Equal(t, "grid-columns", m.Fields[1].Map().Fields[0].Name.ScalarString())
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
|||
318
d2ir/d2ir.go
318
d2ir/d2ir.go
|
|
@ -176,7 +176,7 @@ type Map struct {
|
|||
|
||||
func (m *Map) initRoot() {
|
||||
m.parent = &Field{
|
||||
Name: "root",
|
||||
Name: d2ast.FlatUnquotedString("root"),
|
||||
References: []*FieldReference{{
|
||||
Context_: &RefContext{
|
||||
ScopeMap: m,
|
||||
|
|
@ -293,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":
|
||||
|
|
@ -318,8 +318,9 @@ type Field struct {
|
|||
// *Map.
|
||||
parent Node
|
||||
importAST d2ast.Node
|
||||
suspended bool
|
||||
|
||||
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.
|
||||
|
|
@ -377,11 +378,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"`
|
||||
|
|
@ -409,8 +410,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
|
||||
}
|
||||
|
||||
|
|
@ -428,7 +429,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
|
||||
}
|
||||
}
|
||||
|
|
@ -440,7 +441,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
|
||||
}
|
||||
}
|
||||
|
|
@ -450,21 +451,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 {
|
||||
|
|
@ -473,7 +474,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]) || strings.Contains(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])
|
||||
|
|
@ -488,6 +489,7 @@ type Edge struct {
|
|||
// *Map
|
||||
parent Node
|
||||
importAST d2ast.Node
|
||||
suspended bool
|
||||
|
||||
ID *EdgeID `json:"edge_id"`
|
||||
|
||||
|
|
@ -648,7 +650,41 @@ func (rc *RefContext) EdgeIndex() int {
|
|||
func (rc *RefContext) Equal(rc2 *RefContext) bool {
|
||||
// We intentionally ignore edges here because the same glob can produce multiple RefContexts that should be treated the same with only the edge as the difference.
|
||||
// Same with ScopeMap.
|
||||
return rc.Key.Equals(rc2.Key) && rc.Scope == rc2.Scope && rc.ScopeAST == rc2.ScopeAST
|
||||
if !(rc.Key.Equals(rc2.Key) && rc.Scope == rc2.Scope && rc.ScopeAST == rc2.ScopeAST) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if suspension values match for suspension operations
|
||||
// We don't want these two to equal
|
||||
// 1. *: suspend
|
||||
// 2. *: unsuspend
|
||||
hasSuspension1 := (rc.Key.Primary.Suspension != nil || rc.Key.Value.Suspension != nil)
|
||||
hasSuspension2 := (rc2.Key.Primary.Suspension != nil || rc2.Key.Value.Suspension != nil)
|
||||
|
||||
if hasSuspension1 || hasSuspension2 {
|
||||
var val1, val2 bool
|
||||
if rc.Key.Primary.Suspension != nil {
|
||||
val1 = rc.Key.Primary.Suspension.Value
|
||||
} else if rc.Key.Value.Suspension != nil {
|
||||
val1 = rc.Key.Value.Suspension.Value
|
||||
}
|
||||
|
||||
if rc2.Key.Primary.Suspension != nil {
|
||||
val2 = rc2.Key.Primary.Suspension.Value
|
||||
} else if rc2.Key.Value.Suspension != nil {
|
||||
val2 = rc2.Key.Value.Suspension.Value
|
||||
}
|
||||
|
||||
if hasSuspension1 && hasSuspension2 && val1 != val2 {
|
||||
return false
|
||||
}
|
||||
|
||||
if hasSuspension1 != hasSuspension2 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (m *Map) FieldCountRecursive() int {
|
||||
|
|
@ -669,13 +705,44 @@ func (m *Map) FieldCountRecursive() int {
|
|||
return acc
|
||||
}
|
||||
|
||||
func (m *Map) IsContainer() bool {
|
||||
func (c *compiler) IsContainer(m *Map) bool {
|
||||
if m == nil {
|
||||
return false
|
||||
}
|
||||
// Check references as the fields and edges may not be compiled yet
|
||||
f := m.Parent().(*Field)
|
||||
for _, ref := range f.References {
|
||||
if ref.Primary() && ref.Context_.Key != nil && ref.Context_.Key.Value.Map != nil {
|
||||
for _, n := range ref.Context_.Key.Value.Map.Nodes {
|
||||
if n.MapKey == nil {
|
||||
if n.Import != nil {
|
||||
impn, ok := c.peekImport(n.Import)
|
||||
if ok {
|
||||
for _, f := range impn.Fields {
|
||||
_, isReserved := d2ast.ReservedKeywords[f.Name.ScalarString()]
|
||||
if !(isReserved && f.Name.IsUnquoted()) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
if len(n.MapKey.Edges) > 0 {
|
||||
return true
|
||||
}
|
||||
if n.MapKey.Key != nil {
|
||||
_, isReserved := d2ast.ReservedKeywords[n.MapKey.Key.Path[0].Unbox().ScalarString()]
|
||||
if !(isReserved && f.Name.IsUnquoted()) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, f := range m.Fields {
|
||||
_, isReserved := d2ast.ReservedKeywords[f.Name]
|
||||
if !isReserved {
|
||||
_, isReserved := d2ast.ReservedKeywords[f.Name.ScalarString()]
|
||||
if !(isReserved && f.Name.IsUnquoted()) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
@ -702,9 +769,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()
|
||||
}
|
||||
|
|
@ -712,8 +779,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
|
||||
|
|
@ -722,7 +789,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
|
||||
}
|
||||
|
|
@ -730,14 +797,22 @@ 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 _, isReserved := d2ast.ReservedKeywords[strings.ToLower(s.ScalarString())]; isReserved {
|
||||
if f.Name.IsUnquoted() != s.IsUnquoted() {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if len(rest) == 0 {
|
||||
return f
|
||||
}
|
||||
|
|
@ -751,7 +826,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")
|
||||
|
|
@ -784,10 +859,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 {
|
||||
|
|
@ -841,7 +916,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 {
|
||||
|
|
@ -863,31 +941,37 @@ 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 := d2ast.ReservedKeywords[strings.ToLower(head)]; ok {
|
||||
head = strings.ToLower(head)
|
||||
if _, ok := d2ast.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 must be declared at a board root scope", 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 must be declared at a board root scope", headString)
|
||||
}
|
||||
|
||||
for _, f := range m.Fields {
|
||||
if !strings.EqualFold(f.Name, head) {
|
||||
if !(f.Name != nil && strings.EqualFold(f.Name.ScalarString(), head.ScalarString())) {
|
||||
continue
|
||||
}
|
||||
if _, isReserved := d2ast.ReservedKeywords[strings.ToLower(f.Name.ScalarString())]; isReserved {
|
||||
if f.Name.IsUnquoted() != head.IsUnquoted() {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Don't add references for fake common KeyPath from trimCommon in CreateEdge.
|
||||
if refctx != nil {
|
||||
|
|
@ -922,14 +1006,14 @@ func (m *Map) ensureField(i int, kp *d2ast.KeyPath, refctx *RefContext, create b
|
|||
return nil
|
||||
}
|
||||
shape := ParentShape(m)
|
||||
if _, ok := d2ast.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() {
|
||||
|
|
@ -937,10 +1021,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{}{}
|
||||
|
|
@ -977,9 +1061,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
|
||||
}
|
||||
}
|
||||
|
|
@ -995,7 +1095,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 {
|
||||
|
|
@ -1022,10 +1122,10 @@ func (m *Map) DeleteField(ida ...string) *Field {
|
|||
// then that holder becomes meaningless and should be deleted too
|
||||
parent := ParentField(f)
|
||||
for keywordHolder := range d2ast.ReservedKeywordHolders {
|
||||
if parent != nil && parent.Name == keywordHolder && len(parent.Map().Fields) == 0 {
|
||||
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
|
||||
}
|
||||
|
|
@ -1083,7 +1183,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++ {
|
||||
|
|
@ -1134,10 +1234,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
|
||||
|
|
@ -1179,7 +1279,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++ {
|
||||
|
|
@ -1247,7 +1347,7 @@ func (m *Map) createEdge(eid *EdgeID, refctx *RefContext, gctx *globContext, c *
|
|||
|
||||
if refctx.Edge.Src.HasMultiGlob() {
|
||||
// If src has a double glob we only select leafs, those without children.
|
||||
if src.Map().IsContainer() {
|
||||
if c.IsContainer(src.Map()) {
|
||||
continue
|
||||
}
|
||||
if NodeBoardKind(src) != "" || ParentBoard(src) != ParentBoard(dst) {
|
||||
|
|
@ -1256,7 +1356,7 @@ func (m *Map) createEdge(eid *EdgeID, refctx *RefContext, gctx *globContext, c *
|
|||
}
|
||||
if refctx.Edge.Dst.HasMultiGlob() {
|
||||
// If dst has a double glob we only select leafs, those without children.
|
||||
if dst.Map().IsContainer() {
|
||||
if c.IsContainer(dst.Map()) {
|
||||
continue
|
||||
}
|
||||
if NodeBoardKind(dst) != "" || ParentBoard(src) != ParentBoard(dst) {
|
||||
|
|
@ -1296,7 +1396,7 @@ func (m *Map) createEdge2(eid *EdgeID, refctx *RefContext, gctx *globContext, c
|
|||
return nil, 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++ {
|
||||
|
|
@ -1352,10 +1452,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
|
||||
|
|
@ -1376,7 +1476,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(),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
@ -1385,7 +1485,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
|
||||
|
|
@ -1394,11 +1501,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 = ">"
|
||||
}
|
||||
|
|
@ -1417,7 +1524,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{
|
||||
|
|
@ -1426,7 +1533,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 {
|
||||
|
|
@ -1447,6 +1555,12 @@ func (m *Map) AST() d2ast.Node {
|
|||
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)))
|
||||
}
|
||||
|
|
@ -1458,7 +1572,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
|
||||
}
|
||||
|
|
@ -1517,7 +1631,7 @@ 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) {
|
||||
|
|
@ -1556,7 +1670,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()
|
||||
}
|
||||
|
|
@ -1569,30 +1683,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 := d2ast.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 := d2ast.SimpleReservedKeywords[ida[i]]; ok {
|
||||
if _, ok := d2ast.SimpleReservedKeywords[ida[i].ScalarString()]; ok && ida[i].IsUnquoted() {
|
||||
return i
|
||||
}
|
||||
if _, ok := d2ast.ReservedKeywordHolders[ida[i]]; ok {
|
||||
if _, ok := d2ast.ReservedKeywordHolders[ida[i].ScalarString()]; ok && ida[i].IsUnquoted() {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
|
@ -1636,7 +1750,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:
|
||||
|
|
@ -1657,7 +1771,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:
|
||||
|
|
@ -1678,7 +1792,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:
|
||||
|
|
@ -1688,7 +1802,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)
|
||||
|
|
@ -1700,11 +1814,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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1779,7 +1893,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
|
||||
}
|
||||
|
|
@ -1807,7 +1921,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
|
||||
}
|
||||
|
|
@ -1828,39 +1942,39 @@ func (m *Map) FindBoardRoot(path []string) *Map {
|
|||
return m
|
||||
}
|
||||
|
||||
layersf := m.GetField("layers")
|
||||
scenariosf := m.GetField("scenarios")
|
||||
stepsf := m.GetField("steps")
|
||||
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 == path[0] {
|
||||
if f.Name.ScalarString() == path[0] {
|
||||
if len(path) == 1 {
|
||||
return f.Map()
|
||||
}
|
||||
return layersf.Map().FindBoardRoot(path[1:])
|
||||
return f.Map().FindBoardRoot(path[1:])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if scenariosf != nil && scenariosf.Map() != nil {
|
||||
for _, f := range scenariosf.Map().Fields {
|
||||
if f.Name == path[0] {
|
||||
if f.Name.ScalarString() == path[0] {
|
||||
if len(path) == 1 {
|
||||
return f.Map()
|
||||
}
|
||||
return scenariosf.Map().FindBoardRoot(path[1:])
|
||||
return f.Map().FindBoardRoot(path[1:])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if stepsf != nil && stepsf.Map() != nil {
|
||||
for _, f := range stepsf.Map().Fields {
|
||||
if f.Name == path[0] {
|
||||
if f.Name.ScalarString() == path[0] {
|
||||
if len(path) == 1 {
|
||||
return f.Map()
|
||||
}
|
||||
return stepsf.Map().FindBoardRoot(path[1:])
|
||||
return f.Map().FindBoardRoot(path[1:])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
||||
|
|
|
|||
|
|
@ -111,11 +111,66 @@ func (c *compiler) __import(imp *d2ast.Import) (*Map, bool) {
|
|||
|
||||
c.compileMap(ir, ast, ast)
|
||||
|
||||
// We attempt to resolve variables in the imported file scope first
|
||||
// But ignore errors, in case the variable is meant to be resolved at the
|
||||
// importer
|
||||
savedErrors := make([]d2ast.Error, len(c.err.Errors))
|
||||
copy(savedErrors, c.err.Errors)
|
||||
c.compileSubstitutions(ir, nil)
|
||||
c.err.Errors = savedErrors
|
||||
|
||||
c.seenImports[impPath] = struct{}{}
|
||||
|
||||
return ir, true
|
||||
}
|
||||
|
||||
func (c *compiler) peekImport(imp *d2ast.Import) (*Map, bool) {
|
||||
impPath := imp.PathWithPre()
|
||||
if impPath == "" && imp.Range != (d2ast.Range{}) {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if len(c.importStack) > 0 {
|
||||
if path.Ext(impPath) != ".d2" {
|
||||
impPath += ".d2"
|
||||
}
|
||||
|
||||
if !filepath.IsAbs(impPath) {
|
||||
impPath = path.Join(path.Dir(c.importStack[len(c.importStack)-1]), impPath)
|
||||
}
|
||||
}
|
||||
|
||||
var f fs.File
|
||||
var err error
|
||||
if c.fs == nil {
|
||||
f, err = os.Open(impPath)
|
||||
} else {
|
||||
f, err = c.fs.Open(impPath)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
// Use a separate parse error to avoid polluting the main one
|
||||
localErr := &d2parser.ParseError{}
|
||||
ast, err := d2parser.Parse(impPath, f, &d2parser.ParseOptions{
|
||||
UTF16Pos: c.utf16Pos,
|
||||
ParseError: localErr,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
ir := &Map{}
|
||||
ir.initRoot()
|
||||
ir.parent.(*Field).References[0].Context_.Scope = ast
|
||||
|
||||
c.compileMap(ir, ast, ast)
|
||||
|
||||
return ir, true
|
||||
}
|
||||
|
||||
func nilScopeMap(n Node) {
|
||||
switch n := n.(type) {
|
||||
case *Map:
|
||||
|
|
|
|||
|
|
@ -21,11 +21,14 @@ func (m *Map) multiGlob(pattern []string) ([]*Field, bool) {
|
|||
|
||||
func (m *Map) _doubleGlob(fa *[]*Field) {
|
||||
for _, f := range m.Fields {
|
||||
if _, ok := d2ast.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 := d2ast.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.
|
||||
|
|
@ -45,8 +48,8 @@ func (m *Map) _doubleGlob(fa *[]*Field) {
|
|||
|
||||
func (m *Map) _tripleGlob(fa *[]*Field) {
|
||||
for _, f := range m.Fields {
|
||||
if _, ok := d2ast.ReservedKeywords[f.Name]; ok {
|
||||
if _, ok := d2ast.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.
|
||||
|
|
|
|||
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)
|
||||
})
|
||||
}
|
||||
544
d2js/d2wasm/functions.go
Normal file
544
d2js/d2wasm/functions.go
Normal file
|
|
@ -0,0 +1,544 @@
|
|||
//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/d2animate"
|
||||
"oss.terrastruct.com/d2/d2renderers/d2fonts"
|
||||
"oss.terrastruct.com/d2/d2renderers/d2svg"
|
||||
"oss.terrastruct.com/d2/d2renderers/d2svg/appendix"
|
||||
"oss.terrastruct.com/d2/d2target"
|
||||
"oss.terrastruct.com/d2/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"
|
||||
)
|
||||
|
||||
const DEFAULT_INPUT_PATH = "index"
|
||||
|
||||
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}
|
||||
}
|
||||
|
||||
inputPath := DEFAULT_INPUT_PATH
|
||||
|
||||
if input.InputPath != nil {
|
||||
inputPath = *input.InputPath
|
||||
}
|
||||
|
||||
if _, ok := input.FS[inputPath]; !ok {
|
||||
return nil, &WASMError{Message: fmt.Sprintf("missing '%s' file in input fs", inputPath), 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(inputPath, strings.NewReader(input.FS[inputPath]), &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}
|
||||
}
|
||||
|
||||
compileOpts := &d2lib.CompileOptions{
|
||||
UTF16Pos: true,
|
||||
}
|
||||
|
||||
inputPath := DEFAULT_INPUT_PATH
|
||||
|
||||
if input.InputPath != nil {
|
||||
inputPath = *input.InputPath
|
||||
}
|
||||
|
||||
if _, ok := input.FS[inputPath]; !ok {
|
||||
return nil, &WASMError{Message: fmt.Sprintf("missing '%s' file in input fs", inputPath), Code: 400}
|
||||
}
|
||||
|
||||
compileOpts.InputPath = inputPath
|
||||
|
||||
compileOpts.LayoutResolver = func(engine string) (d2graph.LayoutGraph, error) {
|
||||
switch engine {
|
||||
case "dagre":
|
||||
return d2dagrelayout.DefaultLayout, nil
|
||||
case "elk":
|
||||
return d2elklayout.DefaultLayout, nil
|
||||
default:
|
||||
return nil, &WASMError{Message: fmt.Sprintf("layout option '%s' not recognized", engine), Code: 400}
|
||||
}
|
||||
}
|
||||
|
||||
var err error
|
||||
compileOpts.FS, err = memfs.New(input.FS)
|
||||
if err != nil {
|
||||
return nil, &WASMError{Message: fmt.Sprintf("invalid fs input: %s", err.Error()), Code: 400}
|
||||
}
|
||||
|
||||
var fontRegular []byte
|
||||
var fontItalic []byte
|
||||
var fontBold []byte
|
||||
var fontSemibold []byte
|
||||
if input.Opts != nil && (input.Opts.FontRegular != nil) {
|
||||
fontRegular = *input.Opts.FontRegular
|
||||
}
|
||||
if input.Opts != nil && (input.Opts.FontItalic != nil) {
|
||||
fontItalic = *input.Opts.FontItalic
|
||||
}
|
||||
if input.Opts != nil && (input.Opts.FontBold != nil) {
|
||||
fontBold = *input.Opts.FontBold
|
||||
}
|
||||
if input.Opts != nil && (input.Opts.FontSemibold != nil) {
|
||||
fontSemibold = *input.Opts.FontSemibold
|
||||
}
|
||||
if fontRegular != nil || fontItalic != nil || fontBold != nil || fontSemibold != nil {
|
||||
fontFamily, err := d2fonts.AddFontFamily("custom", fontRegular, fontItalic, fontBold, fontSemibold)
|
||||
if err != nil {
|
||||
return nil, &WASMError{Message: fmt.Sprintf("custom fonts could not be initialized: %s", err.Error()), Code: 400}
|
||||
}
|
||||
compileOpts.FontFamily = fontFamily
|
||||
}
|
||||
|
||||
compileOpts.Ruler, err = textmeasure.NewRuler()
|
||||
if err != nil {
|
||||
return nil, &WASMError{Message: fmt.Sprintf("text ruler cannot be initialized: %s", err.Error()), Code: 500}
|
||||
}
|
||||
|
||||
if input.Opts != nil && input.Opts.Layout != nil {
|
||||
compileOpts.Layout = input.Opts.Layout
|
||||
}
|
||||
|
||||
renderOpts := &d2svg.RenderOpts{}
|
||||
if input.Opts != nil && input.Opts.Sketch != nil {
|
||||
renderOpts.Sketch = input.Opts.Sketch
|
||||
}
|
||||
if input.Opts != nil && input.Opts.Pad != nil {
|
||||
renderOpts.Pad = input.Opts.Pad
|
||||
}
|
||||
if input.Opts != nil && input.Opts.Center != nil {
|
||||
renderOpts.Center = input.Opts.Center
|
||||
}
|
||||
if input.Opts != nil && input.Opts.ThemeID != nil {
|
||||
renderOpts.ThemeID = input.Opts.ThemeID
|
||||
}
|
||||
if input.Opts != nil && input.Opts.DarkThemeID != nil {
|
||||
renderOpts.DarkThemeID = input.Opts.DarkThemeID
|
||||
}
|
||||
if input.Opts != nil && input.Opts.Scale != nil {
|
||||
renderOpts.Scale = input.Opts.Scale
|
||||
}
|
||||
|
||||
ctx := log.WithDefault(context.Background())
|
||||
diagram, g, err := d2lib.Compile(ctx, input.FS[inputPath], compileOpts, renderOpts)
|
||||
if err != nil {
|
||||
if pe, ok := err.(*d2parser.ParseError); ok {
|
||||
errs, _ := json.Marshal(pe.Errors)
|
||||
return nil, &WASMError{Message: string(errs), Code: 400}
|
||||
}
|
||||
return nil, &WASMError{Message: err.Error(), Code: 500}
|
||||
}
|
||||
|
||||
input.FS[inputPath] = d2format.Format(g.AST)
|
||||
|
||||
return CompileResponse{
|
||||
FS: input.FS,
|
||||
InputPath: inputPath,
|
||||
Diagram: *diagram,
|
||||
Graph: *g,
|
||||
RenderOptions: RenderOptions{
|
||||
ThemeID: renderOpts.ThemeID,
|
||||
DarkThemeID: renderOpts.DarkThemeID,
|
||||
Sketch: renderOpts.Sketch,
|
||||
Pad: renderOpts.Pad,
|
||||
Center: renderOpts.Center,
|
||||
Scale: renderOpts.Scale,
|
||||
ForceAppendix: input.Opts.ForceAppendix,
|
||||
Target: input.Opts.Target,
|
||||
AnimateInterval: input.Opts.AnimateInterval,
|
||||
Salt: input.Opts.Salt,
|
||||
NoXMLTag: input.Opts.NoXMLTag,
|
||||
},
|
||||
}, 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}
|
||||
}
|
||||
|
||||
animateInterval := 0
|
||||
if input.Opts != nil && input.Opts.AnimateInterval != nil && *input.Opts.AnimateInterval > 0 {
|
||||
animateInterval = int(*input.Opts.AnimateInterval)
|
||||
}
|
||||
|
||||
var boardPath []string
|
||||
noChildren := true
|
||||
|
||||
if input.Opts.Target != nil {
|
||||
switch *input.Opts.Target {
|
||||
case "*":
|
||||
noChildren = false
|
||||
case "":
|
||||
default:
|
||||
target := *input.Opts.Target
|
||||
if strings.HasSuffix(target, ".*") {
|
||||
target = target[:len(target)-2]
|
||||
noChildren = false
|
||||
}
|
||||
key, err := d2parser.ParseKey(target)
|
||||
if err != nil {
|
||||
return nil, &WASMError{Message: fmt.Sprintf("target '%s' not recognized", target), Code: 400}
|
||||
}
|
||||
boardPath = key.StringIDA()
|
||||
}
|
||||
if !noChildren && animateInterval <= 0 {
|
||||
return nil, &WASMError{Message: fmt.Sprintf("target '%s' only supported for animated SVGs", *input.Opts.Target), Code: 500}
|
||||
}
|
||||
}
|
||||
|
||||
diagram := input.Diagram.GetBoard(boardPath)
|
||||
if diagram == nil {
|
||||
return nil, &WASMError{Message: fmt.Sprintf("render target '%s' not found", strings.Join(boardPath, ".")), Code: 400}
|
||||
}
|
||||
if noChildren {
|
||||
diagram.Layers = nil
|
||||
diagram.Scenarios = nil
|
||||
diagram.Steps = nil
|
||||
}
|
||||
|
||||
renderOpts := &d2svg.RenderOpts{}
|
||||
|
||||
if input.Opts != nil && input.Opts.Salt != nil {
|
||||
renderOpts.Salt = input.Opts.Salt
|
||||
}
|
||||
|
||||
if animateInterval > 0 {
|
||||
masterID, err := diagram.HashID(renderOpts.Salt)
|
||||
if err != nil {
|
||||
return nil, &WASMError{Message: fmt.Sprintf("cannot process animate interval: %s", err.Error()), Code: 500}
|
||||
}
|
||||
renderOpts.MasterID = masterID
|
||||
}
|
||||
|
||||
ruler, err := textmeasure.NewRuler()
|
||||
if err != nil {
|
||||
return nil, &WASMError{Message: fmt.Sprintf("text ruler cannot be initialized: %s", err.Error()), Code: 500}
|
||||
}
|
||||
|
||||
if input.Opts != nil && input.Opts.Sketch != nil {
|
||||
renderOpts.Sketch = input.Opts.Sketch
|
||||
}
|
||||
if input.Opts != nil && input.Opts.Pad != nil {
|
||||
renderOpts.Pad = input.Opts.Pad
|
||||
}
|
||||
if input.Opts != nil && input.Opts.Center != nil {
|
||||
renderOpts.Center = input.Opts.Center
|
||||
}
|
||||
if input.Opts != nil && input.Opts.ThemeID != nil {
|
||||
renderOpts.ThemeID = input.Opts.ThemeID
|
||||
}
|
||||
if input.Opts != nil && input.Opts.DarkThemeID != nil {
|
||||
renderOpts.DarkThemeID = input.Opts.DarkThemeID
|
||||
}
|
||||
if input.Opts != nil && input.Opts.Scale != nil {
|
||||
renderOpts.Scale = input.Opts.Scale
|
||||
}
|
||||
if input.Opts != nil && input.Opts.NoXMLTag != nil {
|
||||
renderOpts.NoXMLTag = input.Opts.NoXMLTag
|
||||
}
|
||||
|
||||
forceAppendix := input.Opts != nil && input.Opts.ForceAppendix != nil && *input.Opts.ForceAppendix
|
||||
|
||||
var boards [][]byte
|
||||
if noChildren {
|
||||
var board []byte
|
||||
board, err = renderSingleBoard(renderOpts, forceAppendix, ruler, diagram)
|
||||
boards = [][]byte{board}
|
||||
} else {
|
||||
boards, err = renderBoards(renderOpts, forceAppendix, ruler, diagram)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, &WASMError{Message: fmt.Sprintf("render failed: %s", err.Error()), Code: 500}
|
||||
}
|
||||
|
||||
var out []byte
|
||||
if len(boards) > 0 {
|
||||
out = boards[0]
|
||||
if animateInterval > 0 {
|
||||
out, err = d2animate.Wrap(diagram, boards, *renderOpts, animateInterval)
|
||||
if err != nil {
|
||||
return nil, &WASMError{Message: fmt.Sprintf("animation failed: %s", err.Error()), Code: 500}
|
||||
}
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func renderSingleBoard(opts *d2svg.RenderOpts, forceAppendix bool, ruler *textmeasure.Ruler, diagram *d2target.Diagram) ([]byte, error) {
|
||||
out, err := d2svg.Render(diagram, opts)
|
||||
if err != nil {
|
||||
return nil, &WASMError{Message: fmt.Sprintf("render failed: %s", err.Error()), Code: 500}
|
||||
}
|
||||
if forceAppendix {
|
||||
out = appendix.Append(diagram, opts, ruler, out)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func renderBoards(opts *d2svg.RenderOpts, forceAppendix bool, ruler *textmeasure.Ruler, diagram *d2target.Diagram) ([][]byte, error) {
|
||||
var boards [][]byte
|
||||
for _, dl := range diagram.Layers {
|
||||
childrenBoards, err := renderBoards(opts, forceAppendix, ruler, dl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
boards = append(boards, childrenBoards...)
|
||||
}
|
||||
for _, dl := range diagram.Scenarios {
|
||||
childrenBoards, err := renderBoards(opts, forceAppendix, ruler, dl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
boards = append(boards, childrenBoards...)
|
||||
}
|
||||
for _, dl := range diagram.Steps {
|
||||
childrenBoards, err := renderBoards(opts, forceAppendix, ruler, dl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
boards = append(boards, childrenBoards...)
|
||||
}
|
||||
|
||||
if !diagram.IsFolderOnly {
|
||||
out, err := renderSingleBoard(opts, forceAppendix, ruler, diagram)
|
||||
if err != nil {
|
||||
return boards, err
|
||||
}
|
||||
boards = append([][]byte{out}, boards...)
|
||||
}
|
||||
return boards, 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
|
||||
}
|
||||
78
d2js/d2wasm/types.go
Normal file
78
d2js/d2wasm/types.go
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
//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"`
|
||||
InputPath *string `json:"inputPath"`
|
||||
Opts *CompileOptions `json:"options"`
|
||||
}
|
||||
|
||||
type RenderOptions struct {
|
||||
Pad *int64 `json:"pad"`
|
||||
Sketch *bool `json:"sketch"`
|
||||
Center *bool `json:"center"`
|
||||
ThemeID *int64 `json:"themeID"`
|
||||
DarkThemeID *int64 `json:"darkThemeID"`
|
||||
Scale *float64 `json:"scale"`
|
||||
ForceAppendix *bool `json:"forceAppendix"`
|
||||
Target *string `json:"target"`
|
||||
AnimateInterval *int64 `json:"animateInterval"`
|
||||
Salt *string `json:"salt"`
|
||||
NoXMLTag *bool `json:"noXMLTag"`
|
||||
}
|
||||
|
||||
type CompileOptions struct {
|
||||
RenderOptions
|
||||
Layout *string `json:"layout"`
|
||||
FontRegular *[]byte `json:"FontRegular"`
|
||||
FontItalic *[]byte `json:"FontItalic"`
|
||||
FontBold *[]byte `json:"FontBold"`
|
||||
FontSemibold *[]byte `json:"FontSemibold"`
|
||||
}
|
||||
|
||||
type CompileResponse struct {
|
||||
FS map[string]string `json:"fs"`
|
||||
InputPath string `json:"inputPath"`
|
||||
Diagram d2target.Diagram `json:"diagram"`
|
||||
Graph d2graph.Graph `json:"graph"`
|
||||
RenderOptions RenderOptions `json:"renderOptions"`
|
||||
}
|
||||
|
||||
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
|
||||
31
d2js/js/CHANGELOG.md
Normal file
31
d2js/js/CHANGELOG.md
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
# 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.**
|
||||
|
||||
## Next
|
||||
|
||||
- Fix TypeScript signatures
|
||||
|
||||
## [0.1.22]
|
||||
### March 20, 2025
|
||||
|
||||
- Support `d2-config`. Support additional options. [#2343](https://github.com/terrastruct/d2/pull/2343)
|
||||
- `themeID`
|
||||
- `darkThemeID`
|
||||
- `center`
|
||||
- `pad`
|
||||
- `scale`
|
||||
- `forceAppendix`
|
||||
- `target`
|
||||
- `animateInterval`
|
||||
- `salt`
|
||||
- `noXMLTag`
|
||||
- Support relative imports. Improve elk error handling [#2382](https://github.com/terrastruct/d2/pull/2382)
|
||||
- Support fonts (`fontRegular`, `fontItalic`, `fontBold`, `fontSemiBold`) [#2384](https://github.com/terrastruct/d2/pull/2384)
|
||||
- Add TypeScript signatures
|
||||
|
||||
## [0.1.21]
|
||||
### January 12, 2025
|
||||
|
||||
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
|
||||
193
d2js/js/README.md
Normal file
193
d2js/js/README.md
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
# 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
|
||||
```
|
||||
|
||||
### Nightly
|
||||
|
||||
Use the `@nightly` tag to get the version that is built by daily CI on the master branch.
|
||||
|
||||
For example,
|
||||
|
||||
```bash
|
||||
yarn add @terrastruct/d2@nightly
|
||||
```
|
||||
|
||||
A demo using the nightly build is hosted [here](https://alixander-d2js.web.val.run/).
|
||||
|
||||
## Usage
|
||||
|
||||
D2.js uses webworkers to call a WASM file.
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```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, result.renderOptions);
|
||||
```
|
||||
|
||||
Configuring render options (see [CompileOptions](#compileoptions) for all available options):
|
||||
|
||||
```javascript
|
||||
import { D2 } from '@terrastruct/d2';
|
||||
|
||||
const d2 = new D2();
|
||||
|
||||
const result = await d2.compile('x -> y', {
|
||||
sketch: true,
|
||||
});
|
||||
const svg = await d2.render(result.diagram, result.renderOptions);
|
||||
```
|
||||
|
||||
### Imports
|
||||
|
||||
In order to support [imports](https://d2lang.com/tour/imports), a mapping of D2 file paths to their content can be passed to the compiler.
|
||||
|
||||
```javascript
|
||||
import { D2 } from '@terrastruct/d2';
|
||||
|
||||
const d2 = new D2();
|
||||
|
||||
const fs = {
|
||||
"project.d2": "a: @import",
|
||||
"import.d2": "x: {shape: circle}",
|
||||
}
|
||||
|
||||
const result = await d2.compile({
|
||||
fs,
|
||||
inputPath: "project.d2",
|
||||
options: {
|
||||
sketch: true
|
||||
}
|
||||
});
|
||||
const svg = await d2.render(result.diagram, result.renderOptions);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `new D2()`
|
||||
|
||||
Creates a new D2 instance.
|
||||
|
||||
### `compile(input: string | CompileRequest, options?: CompileOptions): Promise<CompileResult>`
|
||||
|
||||
Compiles D2 markup into an intermediate representation. It compile options are provided in both `input` and `options`, the latter will take precedence.
|
||||
|
||||
### `render(diagram: Diagram, options?: RenderOptions): Promise<string>`
|
||||
|
||||
Renders a compiled diagram to SVG.
|
||||
|
||||
### `CompileOptions`
|
||||
|
||||
All [RenderOptions](#renderoptions) properties in addition to:
|
||||
|
||||
- `layout`: Layout engine to use ('dagre' | 'elk') [default: 'dagre']
|
||||
- `fontRegular` A byte array containing .ttf file to use for the regular font. If none provided, Source Sans Pro Regular is used.
|
||||
- `fontItalic` A byte array containing .ttf file to use for the italic font. If none provided, Source Sans Pro Italic is used.
|
||||
- `fontBold` A byte array containing .ttf file to use for the bold font. If none provided, Source Sans Pro Bold is used.
|
||||
- `fontSemibold` A byte array containing .ttf file to use for the semibold font. If none provided, Source Sans Pro Semibold is used.
|
||||
|
||||
### `RenderOptions`
|
||||
|
||||
- `sketch`: Enable sketch mode [default: false]
|
||||
- `themeID`: Theme ID to use [default: 0]
|
||||
- `darkThemeID`: Theme ID to use when client is in dark mode
|
||||
- `center`: Center the SVG in the containing viewbox [default: false]
|
||||
- `pad`: Pixels padded around the rendered diagram [default: 100]
|
||||
- `scale`: Scale the output. E.g., 0.5 to halve the default size. The default will render SVG's that will fit to screen. Setting to 1 turns off SVG fitting to screen.
|
||||
- `forceAppendix`: Adds an appendix for tooltips and links [default: false]
|
||||
- `target`: Target board/s to render. If target ends with '*', it will be rendered with all of its scenarios, steps, and layers. Otherwise, only the target board will be rendered. E.g. `target: 'layers.x.*'` to render layer 'x' with all of its children. Pass '*' to render all scenarios, steps, and layers. By default, only the root board is rendered. Multi-board outputs are currently only supported for animated SVGs and so `animateInterval` must be set to a value greater than 0 when targeting multiple boards.
|
||||
- `animateInterval`: If given, multiple boards are packaged as 1 SVG which transitions through each board at the interval (in milliseconds).
|
||||
- `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.
|
||||
- `noXMLTag`: Omit XML tag `(<?xml ...?>)` from output SVG files. Useful when generating SVGs for direct HTML embedding.
|
||||
|
||||
### `CompileRequest`
|
||||
|
||||
- `fs`: A mapping of D2 file paths to their content
|
||||
- `inputPath`: The path of the entry D2 file [default: index]
|
||||
- `options`: The [CompileOptions](#compileoptions) to pass to the compiler
|
||||
|
||||
### `CompileResult`
|
||||
|
||||
- `diagram`: `Diagram`: Compiled D2 diagram
|
||||
- `options`: `RenderOptions`: Render options merged with configuration set in diagram
|
||||
- `fs`
|
||||
- `graph`
|
||||
|
||||
## 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.
|
||||
|
||||
### Publishing
|
||||
|
||||
TODO stable release publishing.
|
||||
|
||||
Nightly builds are automated by CI by running:
|
||||
|
||||
```bash
|
||||
PUBLISH=1 ./make.sh build
|
||||
```
|
||||
|
||||
## 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.
75
d2js/js/ci/build.sh
Executable file
75
d2js/js/ci/build.sh
Executable file
|
|
@ -0,0 +1,75 @@
|
|||
#!/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
|
||||
|
||||
if [ -n "${NPM_VERSION:-}" ]; then
|
||||
cp package.json package.json.bak
|
||||
trap 'rm -f .npmrc; mv package.json.bak package.json' EXIT
|
||||
|
||||
if [ "$NPM_VERSION" = "nightly" ]; then
|
||||
echo "Publishing nightly version to npm..."
|
||||
|
||||
DATE_TAG=$(date +'%Y%m%d')
|
||||
COMMIT_SHORT=$(git rev-parse --short HEAD)
|
||||
CURRENT_VERSION=$(node -p "require('./package.json').version")
|
||||
PUBLISH_VERSION="${CURRENT_VERSION}-nightly.${DATE_TAG}.${COMMIT_SHORT}"
|
||||
NPM_TAG="nightly"
|
||||
|
||||
echo "Updating package version to ${PUBLISH_VERSION}"
|
||||
else
|
||||
echo "Publishing official version ${NPM_VERSION} to npm..."
|
||||
PUBLISH_VERSION="$NPM_VERSION"
|
||||
NPM_TAG="latest"
|
||||
|
||||
echo "Setting package version to ${PUBLISH_VERSION}"
|
||||
fi
|
||||
|
||||
# Update package.json with the new version
|
||||
npm version "${PUBLISH_VERSION}" --no-git-tag-version
|
||||
|
||||
echo "Publishing to npm with tag '${NPM_TAG}'..."
|
||||
if [ -n "${NPM_TOKEN-}" ]; then
|
||||
# Create .npmrc file with auth token
|
||||
echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > .npmrc
|
||||
|
||||
if npm publish --tag "$NPM_TAG"; then
|
||||
echo "Successfully published @terrastruct/d2@${PUBLISH_VERSION} to npm with tag '${NPM_TAG}'"
|
||||
|
||||
# For official releases, bump the patch version
|
||||
if [ "$NPM_VERSION" != "nightly" ]; then
|
||||
# Restore original package.json first
|
||||
mv package.json.bak package.json
|
||||
|
||||
echo "Bumping version to ${NPM_VERSION}"
|
||||
npm version "${NPM_VERSION}" --no-git-tag-version
|
||||
git add package.json
|
||||
git commit -m "Bump version to ${NPM_VERSION} [skip ci]"
|
||||
|
||||
# Cancel the trap since we manually restored and don't want it to execute on exit
|
||||
trap - EXIT
|
||||
fi
|
||||
else
|
||||
echoerr "Failed to publish package to npm"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echoerr "NPM_TOKEN environment variable is required for publishing to npm"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
BIN
d2js/js/d2.wasm
Executable file
BIN
d2js/js/d2.wasm
Executable file
Binary file not shown.
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, result.renderOptions);
|
||||
document.getElementById("output").innerHTML = svg;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
document.getElementById("output").textContent = err.message;
|
||||
}
|
||||
};
|
||||
compile();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
459
d2js/js/examples/customizable.html
Normal file
459
d2js/js/examples/customizable.html
Normal file
|
|
@ -0,0 +1,459 @@
|
|||
<!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;
|
||||
}
|
||||
|
||||
.option:has(.option-toggle-box:not(:checked)) .option-select {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.option {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.input-label,
|
||||
.checkbox-label,
|
||||
.select-label {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.checkbox-label,
|
||||
.select-label {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.text-input,
|
||||
.number-input {
|
||||
width: 3rem;
|
||||
}
|
||||
|
||||
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 {
|
||||
min-width: 100%;
|
||||
max-height: 90vh;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="controls">
|
||||
<textarea id="input">x -> y</textarea>
|
||||
<div class="options-group">
|
||||
<div class="option">
|
||||
<div class="option-toggle">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="layout-toggle" class="option-toggle-box" />
|
||||
<span>Layout</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="option-select">
|
||||
<div class="radio-group">
|
||||
<label class="radio-label">
|
||||
<input type="radio" name="layout-select" value="dagre" checked />
|
||||
Dagre
|
||||
</label>
|
||||
<label class="radio-label">
|
||||
<input type="radio" name="layout-select" value="elk" />
|
||||
ELK
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="option">
|
||||
<div class="option-toggle">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="sketch-toggle" class="option-toggle-box" />
|
||||
<span>Sketch Mode</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="option-select">
|
||||
<div class="radio-group">
|
||||
<label class="radio-label">
|
||||
<input type="radio" name="sketch-select" value="true" checked />
|
||||
Enabled
|
||||
</label>
|
||||
<label class="radio-label">
|
||||
<input type="radio" name="sketch-select" value="false" />
|
||||
Disabled
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="option">
|
||||
<div class="option-toggle">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="center-toggle" class="option-toggle-box" />
|
||||
<span>Centered</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="option-select">
|
||||
<div class="radio-group">
|
||||
<label class="radio-label">
|
||||
<input type="radio" name="center-select" value="true" checked />
|
||||
Enabled
|
||||
</label>
|
||||
<label class="radio-label">
|
||||
<input type="radio" name="center-select" value="false" />
|
||||
Disabled
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="option">
|
||||
<div class="option-toggle">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="appendix-toggle" class="option-toggle-box" />
|
||||
<span>Force Appendix</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="option-select">
|
||||
<div class="radio-group">
|
||||
<label class="radio-label">
|
||||
<input type="radio" name="appendix-select" value="true" checked />
|
||||
Enabled
|
||||
</label>
|
||||
<label class="radio-label">
|
||||
<input type="radio" name="appendix-select" value="false" />
|
||||
Disabled
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="option">
|
||||
<div class="option-toggle">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="theme-toggle" class="option-toggle-box" />
|
||||
<span>Theme</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="option-select">
|
||||
<select id="theme-select">
|
||||
<option selected value="0">Default</option>
|
||||
<option value="1">Neutral grey</option>
|
||||
<option value="3">Flagship Terrastruct</option>
|
||||
<option value="4">Cool classics</option>
|
||||
<option value="5">Mixed berry blue</option>
|
||||
<option value="6">Grape soda</option>
|
||||
<option value="7">Aubergine</option>
|
||||
<option value="8">Colorblind clear</option>
|
||||
<option value="100">Vanilla nitro cola</option>
|
||||
<option value="101">Orange creamsicle</option>
|
||||
<option value="102">Shirley temple</option>
|
||||
<option value="103">Earth tones</option>
|
||||
<option value="104">Everglade green</option>
|
||||
<option value="105">Buttered toast</option>
|
||||
<option value="200">Dark mauve</option>
|
||||
<option value="300">Terminal</option>
|
||||
<option value="301">Terminal grayscale</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="option">
|
||||
<div class="option-toggle">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="dark-theme-toggle" class="option-toggle-box" />
|
||||
<span>Dark Theme</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="option-select">
|
||||
<select id="dark-theme-select">
|
||||
<option selected value="0">Default</option>
|
||||
<option value="1">Neutral grey</option>
|
||||
<option value="3">Flagship Terrastruct</option>
|
||||
<option value="4">Cool classics</option>
|
||||
<option value="5">Mixed berry blue</option>
|
||||
<option value="6">Grape soda</option>
|
||||
<option value="7">Aubergine</option>
|
||||
<option value="8">Colorblind clear</option>
|
||||
<option value="100">Vanilla nitro cola</option>
|
||||
<option value="101">Orange creamsicle</option>
|
||||
<option value="102">Shirley temple</option>
|
||||
<option value="103">Earth tones</option>
|
||||
<option value="104">Everglade green</option>
|
||||
<option value="105">Buttered toast</option>
|
||||
<option value="200">Dark mauve</option>
|
||||
<option value="300">Terminal</option>
|
||||
<option value="301">Terminal grayscale</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="option">
|
||||
<div class="option-toggle">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="pad-toggle" class="option-toggle-box" />
|
||||
<span>Padding</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="option-select">
|
||||
<label class="input-label">
|
||||
<input
|
||||
type="number"
|
||||
id="pad-input"
|
||||
value="20"
|
||||
step="10"
|
||||
class="number-input"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="option">
|
||||
<div class="option-toggle">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="scale-toggle" class="option-toggle-box" />
|
||||
<span>Scale</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="option-select">
|
||||
<label class="input-label">
|
||||
<input
|
||||
type="number"
|
||||
id="scale-input"
|
||||
value="1"
|
||||
step="0.1"
|
||||
min="0"
|
||||
class="number-input"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="option">
|
||||
<div class="option-toggle">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="target-toggle" class="option-toggle-box" />
|
||||
<span>Target</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="option-select">
|
||||
<label class="input-label">
|
||||
<input type="text" id="target-input" class="text-input" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="option">
|
||||
<div class="option-toggle">
|
||||
<label class="checkbox-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="animate-interval-toggle"
|
||||
class="option-toggle-box"
|
||||
/>
|
||||
<span>Animate Interval</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="option-select">
|
||||
<label class="input-label">
|
||||
<input
|
||||
type="number"
|
||||
id="animate-interval-input"
|
||||
value="0"
|
||||
step="100"
|
||||
min="0"
|
||||
class="number-input"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="option">
|
||||
<div class="option-select">
|
||||
<label class="input-label">
|
||||
<span>Salt</span>
|
||||
<input type="text" id="salt-input" class="text-input" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="option">
|
||||
<div class="option-select">
|
||||
<label class="input-label">
|
||||
<span>Regular Font</span>
|
||||
<input
|
||||
type="file"
|
||||
accept=".ttf"
|
||||
id="font-regular-input"
|
||||
class="file-input"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="option">
|
||||
<div class="option-select">
|
||||
<label class="input-label">
|
||||
<span>Italic Font</span>
|
||||
<input
|
||||
type="file"
|
||||
accept=".ttf"
|
||||
id="font-italic-input"
|
||||
class="file-input"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="option">
|
||||
<div class="option-select">
|
||||
<label class="input-label">
|
||||
<span>Bold Font</span>
|
||||
<input type="file" accept=".ttf" id="font-bold-input" class="file-input" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="option">
|
||||
<div class="option-select">
|
||||
<label class="input-label">
|
||||
<span>Semibold Font</span>
|
||||
<input
|
||||
type="file"
|
||||
accept=".ttf"
|
||||
id="font-semibold-input"
|
||||
class="file-input"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</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();
|
||||
const loadFont = async (file) => {
|
||||
if (file != undefined) {
|
||||
const font = await file.arrayBuffer();
|
||||
return Array.from(new Uint8Array(font));
|
||||
}
|
||||
};
|
||||
window.compile = async () => {
|
||||
const input = document.getElementById("input").value;
|
||||
const layout = document.getElementById("layout-toggle").checked
|
||||
? document.querySelector('input[name="layout-select"]:checked').value
|
||||
: null;
|
||||
const sketch = document.getElementById("sketch-toggle").checked
|
||||
? document.querySelector('input[name="sketch-select"]:checked').value == "true"
|
||||
: null;
|
||||
const center = document.getElementById("center-toggle").checked
|
||||
? document.querySelector('input[name="center-select"]:checked').value == "true"
|
||||
: null;
|
||||
const forceAppendix = document.getElementById("appendix-toggle").checked
|
||||
? document.querySelector('input[name="appendix-select"]:checked').value ==
|
||||
"true"
|
||||
: null;
|
||||
const themeSelector = document.getElementById("theme-select");
|
||||
const themeId = document.getElementById("theme-toggle").checked
|
||||
? Number(themeSelector.options[themeSelector.selectedIndex].value)
|
||||
: null;
|
||||
const darkThemeSelector = document.getElementById("dark-theme-select");
|
||||
const darkThemeId = document.getElementById("dark-theme-toggle").checked
|
||||
? Number(darkThemeSelector.options[darkThemeSelector.selectedIndex].value)
|
||||
: null;
|
||||
const pad = document.getElementById("pad-toggle").checked
|
||||
? Number(document.getElementById("pad-input").value)
|
||||
: null;
|
||||
const scale = document.getElementById("scale-toggle").checked
|
||||
? Number(document.getElementById("scale-input").value)
|
||||
: null;
|
||||
const target = document.getElementById("target-toggle").checked
|
||||
? String(document.getElementById("target-input").value)
|
||||
: null;
|
||||
const animateInterval = document.getElementById("animate-interval-toggle").checked
|
||||
? Number(document.getElementById("animate-interval-input").value)
|
||||
: null;
|
||||
const salt = String(document.getElementById("salt-input").value);
|
||||
const fontRegular = await loadFont(
|
||||
document.getElementById("font-regular-input").files[0]
|
||||
);
|
||||
const fontItalic = await loadFont(
|
||||
document.getElementById("font-italic-input").files[0]
|
||||
);
|
||||
const fontBold = await loadFont(
|
||||
document.getElementById("font-bold-input").files[0]
|
||||
);
|
||||
const fontSemibold = await loadFont(
|
||||
document.getElementById("font-semibold-input").files[0]
|
||||
);
|
||||
try {
|
||||
const result = await d2.compile(input, {
|
||||
layout,
|
||||
sketch,
|
||||
themeId,
|
||||
darkThemeId,
|
||||
scale,
|
||||
pad,
|
||||
center,
|
||||
forceAppendix,
|
||||
target,
|
||||
animateInterval,
|
||||
salt,
|
||||
fontRegular,
|
||||
fontItalic,
|
||||
fontSemibold,
|
||||
fontBold,
|
||||
noXmlTag: true,
|
||||
});
|
||||
const svg = await d2.render(result.diagram, result.renderOptions);
|
||||
document.getElementById("output").innerHTML = svg;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
document.getElementById("output").textContent = err.message;
|
||||
}
|
||||
};
|
||||
compile();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
343
d2js/js/index.d.ts
vendored
Normal file
343
d2js/js/index.d.ts
vendored
Normal file
|
|
@ -0,0 +1,343 @@
|
|||
export class D2 {
|
||||
compile(input: string, options?: Omit<CompileRequest, "fs">): Promise<CompileResponse>;
|
||||
compile(input: CompileRequest): Promise<CompileResponse>;
|
||||
|
||||
render(diagram: Diagram, options?: RenderOptions): Promise<string>;
|
||||
}
|
||||
|
||||
export interface RenderOptions {
|
||||
/** Enable sketch mode [default: false] */
|
||||
sketch?: boolean;
|
||||
/** Theme ID to use [default: 0] */
|
||||
themeID?: number;
|
||||
/** Theme ID to use when client is in dark mode */
|
||||
darkThemeID?: number;
|
||||
/** Center the SVG in the containing viewbox [default: false] */
|
||||
center?: boolean;
|
||||
/** Pixels padded around the rendered diagram [default: 100] */
|
||||
pad?: number;
|
||||
/** Scale the output. E.g., 0.5 to halve the default size. The default will render SVG's that will fit to screen. Setting to 1 turns off SVG fitting to screen. */
|
||||
scale?: number;
|
||||
/** Adds an appendix for tooltips and links [default: false] */
|
||||
forceAppendix?: boolean;
|
||||
/** Target board/s to render. If target ends with '', it will be rendered with all of its scenarios, steps, and layers. Otherwise, only the target board will be rendered. E.g. target: 'layers.x.*' to render layer 'x' with all of its children. Pass '' to render all scenarios, steps, and layers. By default, only the root board is rendered. Multi-board outputs are currently only supported for animated SVGs and so animateInterval must be set to a value greater than 0 when targeting multiple boards. */
|
||||
target?: string;
|
||||
/** If given, multiple boards are packaged as 1 SVG which transitions through each board at the interval (in milliseconds). */
|
||||
animateInterval?: number;
|
||||
/** 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. */
|
||||
salt?: string;
|
||||
/** Omit XML tag (<?xml ...?>) from output SVG files. Useful when generating SVGs for direct HTML embedding. */
|
||||
noXMLTag?: boolean;
|
||||
}
|
||||
|
||||
export interface CompileOptions extends RenderOptions {
|
||||
/** Layout engine to use [default: 'dagre'] */
|
||||
layout?: "dagre" | "elk";
|
||||
/** A byte array containing .ttf file to use for the regular font. If none provided, Source Sans Pro Regular is used. */
|
||||
fontRegular?: Uint8Array;
|
||||
/** A byte array containing .ttf file to use for the italic font. If none provided, Source Sans Pro Italic is used. */
|
||||
fontItalic?: Uint8Array;
|
||||
/** A byte array containing .ttf file to use for the bold font. If none provided, Source Sans Pro Bold is used. */
|
||||
fontBold?: Uint8Array;
|
||||
/** A byte array containing .ttf file to use for the semibold font. If none provided, Source Sans Pro Semibold is used. */
|
||||
fontSemibold?: Uint8Array;
|
||||
}
|
||||
|
||||
export interface CompileRequest {
|
||||
/** A mapping of D2 file paths to their content*/
|
||||
fs: Record<string, string>;
|
||||
/** The path of the entry D2 file [default: index]*/
|
||||
inputPath?: string;
|
||||
/** The CompileOptions to pass to the compiler*/
|
||||
options: CompileOptions;
|
||||
}
|
||||
|
||||
export interface CompileResponse {
|
||||
/** Compiled D2 diagram*/
|
||||
diagram: Diagram /* d2target.Diagram */;
|
||||
/** RenderOptions: Render options merged with configuration set in diagram*/
|
||||
renderOptions: RenderOptions;
|
||||
fs: Record<string, string>;
|
||||
graph: Graph;
|
||||
inputPath: string;
|
||||
}
|
||||
|
||||
export interface Diagram {
|
||||
config?: RenderOptions;
|
||||
name: string;
|
||||
/**
|
||||
* See docs on the same field in d2graph to understand what it means.
|
||||
*/
|
||||
isFolderOnly: boolean;
|
||||
description?: string;
|
||||
fontFamily?: any /* d2fonts.FontFamily */;
|
||||
shapes: Shape[];
|
||||
connections: Connection[];
|
||||
root: Shape;
|
||||
legend?: Legend;
|
||||
layers?: (Diagram | undefined)[];
|
||||
scenarios?: (Diagram | undefined)[];
|
||||
steps?: (Diagram | undefined)[];
|
||||
}
|
||||
|
||||
export interface Legend {
|
||||
shapes?: Shape[];
|
||||
connections?: Connection[];
|
||||
}
|
||||
|
||||
export type Shape = (Class | SQLTable | Text) & ShapeBase;
|
||||
|
||||
export interface ShapeBase {
|
||||
id: string;
|
||||
type: string;
|
||||
classes?: string[];
|
||||
pos: Point;
|
||||
width: number /* int */;
|
||||
height: number /* int */;
|
||||
opacity: number /* float64 */;
|
||||
strokeDash: number /* float64 */;
|
||||
strokeWidth: number /* int */;
|
||||
borderRadius: number /* int */;
|
||||
fill: string;
|
||||
fillPattern?: string;
|
||||
stroke: string;
|
||||
animated: boolean;
|
||||
shadow: boolean;
|
||||
"3d": boolean;
|
||||
multiple: boolean;
|
||||
"double-border": boolean;
|
||||
tooltip: string;
|
||||
link: string;
|
||||
prettyLink?: string;
|
||||
icon?: string /* url.URL */;
|
||||
iconPosition: string;
|
||||
/**
|
||||
* Whether the shape should allow shapes behind it to bleed through
|
||||
* Currently just used for sequence diagram groups
|
||||
*/
|
||||
blend: boolean;
|
||||
contentAspectRatio?: number /* float64 */;
|
||||
labelPosition?: string;
|
||||
zIndex: number /* int */;
|
||||
level: number /* int */;
|
||||
/**
|
||||
* These are used for special shapes, sql_table and class
|
||||
*/
|
||||
primaryAccentColor?: string;
|
||||
secondaryAccentColor?: string;
|
||||
neutralAccentColor?: string;
|
||||
}
|
||||
|
||||
export interface Point {
|
||||
x: number /* int */;
|
||||
y: number /* int */;
|
||||
}
|
||||
|
||||
export interface Class {
|
||||
fields: ClassField[];
|
||||
methods: ClassMethod[];
|
||||
}
|
||||
|
||||
export interface ClassField {
|
||||
name: string;
|
||||
type: string;
|
||||
visibility: string;
|
||||
}
|
||||
|
||||
export interface ClassMethod {
|
||||
name: string;
|
||||
return: string;
|
||||
visibility: string;
|
||||
}
|
||||
|
||||
export interface SQLTable {
|
||||
columns: SQLColumn[];
|
||||
}
|
||||
|
||||
export interface SQLColumn {
|
||||
name: Text;
|
||||
type: Text;
|
||||
constraint: string[];
|
||||
reference: string;
|
||||
}
|
||||
|
||||
export interface Text {
|
||||
label: string;
|
||||
fontSize: number /* int */;
|
||||
fontFamily: string;
|
||||
language: string;
|
||||
color: string;
|
||||
italic: boolean;
|
||||
bold: boolean;
|
||||
underline: boolean;
|
||||
labelWidth: number /* int */;
|
||||
labelHeight: number /* int */;
|
||||
labelFill?: string;
|
||||
}
|
||||
|
||||
export interface Connection extends Text {
|
||||
id: string;
|
||||
classes?: string[];
|
||||
src: string;
|
||||
srcArrow: Arrowhead;
|
||||
srcLabel?: Text;
|
||||
dst: string;
|
||||
dstArrow: Arrowhead;
|
||||
dstLabel?: Text;
|
||||
opacity: number /* float64 */;
|
||||
strokeDash: number /* float64 */;
|
||||
strokeWidth: number /* int */;
|
||||
stroke: string;
|
||||
fill?: string;
|
||||
borderRadius?: number /* float64 */;
|
||||
labelPosition: string;
|
||||
labelPercentage: number /* float64 */;
|
||||
link: string;
|
||||
prettyLink?: string;
|
||||
route: (any /* geo.Point */ | undefined)[];
|
||||
isCurve?: boolean;
|
||||
animated: boolean;
|
||||
tooltip: string;
|
||||
icon?: string /* url.URL */;
|
||||
iconPosition?: string;
|
||||
zIndex: number /* int */;
|
||||
}
|
||||
|
||||
export type Arrowhead =
|
||||
| "none"
|
||||
| "arrow"
|
||||
| "unfilled-triangle"
|
||||
| "triangle"
|
||||
| "diamond"
|
||||
| "filled-diamond"
|
||||
| "circle"
|
||||
| "filled-circle"
|
||||
| "box"
|
||||
| "filled-box"
|
||||
| "line"
|
||||
| "cf-one"
|
||||
| "cf-many"
|
||||
| "cf-one-required"
|
||||
| "cf-many-required";
|
||||
|
||||
export interface Graph {
|
||||
name: string;
|
||||
/**
|
||||
* IsFolderOnly indicates a board or scenario itself makes no modifications from its
|
||||
* base. Folder only boards do not have a render and are used purely for organizing
|
||||
* the board tree.
|
||||
*/
|
||||
isFolderOnly: boolean;
|
||||
ast?: any /* d2ast.Map */;
|
||||
root?: Object;
|
||||
legend?: Legend;
|
||||
edges: (Edge | undefined)[];
|
||||
objects: (Object | undefined)[];
|
||||
layers?: (Graph | undefined)[];
|
||||
scenarios?: (Graph | undefined)[];
|
||||
steps?: (Graph | undefined)[];
|
||||
theme?: any /* d2themes.Theme */;
|
||||
/**
|
||||
* Object.Level uses the location of a nested graph
|
||||
*/
|
||||
rootLevel?: number /* int */;
|
||||
/**
|
||||
* 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?: { [key: string]: any };
|
||||
}
|
||||
|
||||
export interface Edge {
|
||||
index: number /* int */;
|
||||
srcTableColumnIndex?: number /* int */;
|
||||
dstTableColumnIndex?: number /* int */;
|
||||
labelPosition?: string;
|
||||
labelPercentage?: number /* float64 */;
|
||||
isCurve: boolean;
|
||||
route?: (any /* geo.Point */ | undefined)[];
|
||||
src_arrow: boolean;
|
||||
srcArrowhead?: Attributes;
|
||||
/**
|
||||
* TODO alixander (Mon Sep 12 2022): deprecate SrcArrow and DstArrow and just use SrcArrowhead and DstArrowhead
|
||||
*/
|
||||
dst_arrow: boolean;
|
||||
dstArrowhead?: Attributes;
|
||||
references?: EdgeReference[];
|
||||
attributes?: Attributes;
|
||||
zIndex: number /* int */;
|
||||
}
|
||||
|
||||
export interface Attributes {
|
||||
label: Scalar;
|
||||
labelDimensions: TextDimensions;
|
||||
style: Style;
|
||||
icon?: string /* url.URL */;
|
||||
tooltip?: Scalar;
|
||||
link?: Scalar;
|
||||
width?: Scalar;
|
||||
height?: Scalar;
|
||||
top?: Scalar;
|
||||
left?: Scalar;
|
||||
/**
|
||||
* TODO consider separate Attributes struct for shape-specific and edge-specific
|
||||
* Shapes only
|
||||
*/
|
||||
near_key?: any /* d2ast.KeyPath */;
|
||||
language?: string;
|
||||
/**
|
||||
* TODO: default to ShapeRectangle instead of empty string
|
||||
*/
|
||||
shape: Scalar;
|
||||
direction: Scalar;
|
||||
constraint: string[];
|
||||
gridRows?: Scalar;
|
||||
gridColumns?: Scalar;
|
||||
gridGap?: Scalar;
|
||||
verticalGap?: Scalar;
|
||||
horizontalGap?: Scalar;
|
||||
labelPosition?: Scalar;
|
||||
iconPosition?: Scalar;
|
||||
/**
|
||||
* These names are attached to the rendered elements in SVG
|
||||
* so that users can target them however they like outside of D2
|
||||
*/
|
||||
classes?: string[];
|
||||
}
|
||||
|
||||
export interface EdgeReference {
|
||||
map_key_edge_index: number /* int */;
|
||||
}
|
||||
|
||||
export interface Scalar {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface Style {
|
||||
opacity?: Scalar;
|
||||
stroke?: Scalar;
|
||||
fill?: Scalar;
|
||||
fillPattern?: Scalar;
|
||||
strokeWidth?: Scalar;
|
||||
strokeDash?: Scalar;
|
||||
borderRadius?: Scalar;
|
||||
shadow?: Scalar;
|
||||
"3d"?: Scalar;
|
||||
multiple?: Scalar;
|
||||
font?: Scalar;
|
||||
fontSize?: Scalar;
|
||||
fontColor?: Scalar;
|
||||
animated?: Scalar;
|
||||
bold?: Scalar;
|
||||
italic?: Scalar;
|
||||
underline?: Scalar;
|
||||
filled?: Scalar;
|
||||
doubleBorder?: Scalar;
|
||||
textTransform?: Scalar;
|
||||
}
|
||||
|
||||
export interface TextDimensions {
|
||||
width: number /* int */;
|
||||
height: number /* int */;
|
||||
}
|
||||
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 "$@"
|
||||
61
d2js/js/package.json
Normal file
61
d2js/js/package.json
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
{
|
||||
"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.23",
|
||||
"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",
|
||||
"types": "./index.d.ts"
|
||||
},
|
||||
"require": {
|
||||
"default": "./dist/node-cjs/index.js",
|
||||
"types": "./index.d.ts"
|
||||
},
|
||||
"default": "./dist/node-esm/index.js"
|
||||
},
|
||||
"./worker": "./dist/browser/worker.js"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"index.d.ts"
|
||||
],
|
||||
"types": "./index.d.ts",
|
||||
"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"
|
||||
},
|
||||
"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
102
d2js/js/src/index.js
Normal file
102
d2js/js/src/index.js
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
import { createWorker, loadFile } from "./platform.js";
|
||||
|
||||
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 request =
|
||||
typeof input === "string"
|
||||
? { fs: { index: input }, options }
|
||||
: { ...input, options: { ...options, ...input.options } };
|
||||
return this.sendMessage("compile", request);
|
||||
}
|
||||
|
||||
async render(diagram, options = {}) {
|
||||
return this.sendMessage("render", { diagram, options });
|
||||
}
|
||||
|
||||
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"));
|
||||
}
|
||||
82
d2js/js/src/worker.browser.js
Normal file
82
d2js/js/src/worker.browser.js
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
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 the layout option has not been set, we generate the elk layout now
|
||||
// anyway to support `layout-engine: elk` in d2-config vars
|
||||
if (data.options.layout === "elk" || data.options.layout == null) {
|
||||
const elkGraph = await d2.getELKGraph(JSON.stringify(data));
|
||||
const response = JSON.parse(elkGraph);
|
||||
if (response.error) throw new Error(response.error.message);
|
||||
const elkGraph2 = response.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);
|
||||
const decoded = new TextDecoder().decode(
|
||||
Uint8Array.from(atob(response.data), (c) => c.charCodeAt(0))
|
||||
);
|
||||
currentPort.postMessage({ type: "result", data: decoded });
|
||||
} 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);
|
||||
70
d2js/js/src/worker.js
Normal file
70
d2js/js/src/worker.js
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
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 {
|
||||
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);
|
||||
77
d2js/js/src/worker.node.js
Normal file
77
d2js/js/src/worker.node.js
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
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" || data.options.layout == null) {
|
||||
const elkGraph = await d2.getELKGraph(JSON.stringify(data));
|
||||
const response = JSON.parse(elkGraph);
|
||||
if (response.error) throw new Error(response.error.message);
|
||||
const elkGraph2 = response.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);
|
||||
const decoded = new TextDecoder().decode(
|
||||
Uint8Array.from(atob(response.data), (c) => c.charCodeAt(0))
|
||||
);
|
||||
currentPort.postMessage({ type: "result", data: decoded });
|
||||
} 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);
|
||||
});
|
||||
234
d2js/js/test/unit/basic.test.js
Normal file
234
d2js/js/test/unit/basic.test.js
Normal file
|
|
@ -0,0 +1,234 @@
|
|||
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("import works", async () => {
|
||||
const d2 = new D2();
|
||||
const fs = {
|
||||
index: "a: @import",
|
||||
"import.d2": "x: {shape: circle}",
|
||||
};
|
||||
const result = await d2.compile({ fs });
|
||||
expect(result.diagram).toBeDefined();
|
||||
await d2.worker.terminate();
|
||||
}, 20000);
|
||||
|
||||
test("relative import works", async () => {
|
||||
const d2 = new D2();
|
||||
const fs = {
|
||||
"folder/index.d2": "a: @../import",
|
||||
"import.d2": "x: {shape: circle}",
|
||||
};
|
||||
const inputPath = "folder/index.d2";
|
||||
const result = await d2.compile({ fs, inputPath });
|
||||
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("d2-config read correctly", async () => {
|
||||
const d2 = new D2();
|
||||
const result = await d2.compile(
|
||||
`
|
||||
vars: {
|
||||
d2-config: {
|
||||
theme-id: 4
|
||||
dark-theme-id: 200
|
||||
pad: 10
|
||||
center: true
|
||||
sketch: true
|
||||
layout-engine: elk
|
||||
}
|
||||
}
|
||||
x -> y
|
||||
`
|
||||
);
|
||||
expect(result.renderOptions.sketch).toBe(true);
|
||||
expect(result.renderOptions.themeID).toBe(4);
|
||||
expect(result.renderOptions.darkThemeID).toBe(200);
|
||||
expect(result.renderOptions.center).toBe(true);
|
||||
expect(result.renderOptions.pad).toBe(10);
|
||||
await d2.worker.terminate();
|
||||
}, 20000);
|
||||
|
||||
test("render options take priority", async () => {
|
||||
const d2 = new D2();
|
||||
const result = await d2.compile(
|
||||
`
|
||||
vars: {
|
||||
d2-config: {
|
||||
theme-id: 4
|
||||
dark-theme-id: 200
|
||||
pad: 10
|
||||
center: true
|
||||
sketch: true
|
||||
layout-engine: elk
|
||||
}
|
||||
}
|
||||
x -> y
|
||||
`,
|
||||
{
|
||||
sketch: false,
|
||||
themeID: 100,
|
||||
darkThemeID: 300,
|
||||
center: false,
|
||||
pad: 0,
|
||||
layout: "dagre",
|
||||
}
|
||||
);
|
||||
expect(result.renderOptions.sketch).toBe(false);
|
||||
expect(result.renderOptions.themeID).toBe(100);
|
||||
expect(result.renderOptions.darkThemeID).toBe(300);
|
||||
expect(result.renderOptions.center).toBe(false);
|
||||
expect(result.renderOptions.pad).toBe(0);
|
||||
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("center render works", async () => {
|
||||
const d2 = new D2();
|
||||
const result = await d2.compile("x -> y", { center: true });
|
||||
const svg = await d2.render(result.diagram, { center: true });
|
||||
expect(svg).toContain("<svg");
|
||||
expect(svg).toContain("</svg>");
|
||||
expect(svg).toContain("xMidYMid meet");
|
||||
await d2.worker.terminate();
|
||||
}, 20000);
|
||||
|
||||
test("no XML tag works", async () => {
|
||||
const d2 = new D2();
|
||||
const result = await d2.compile("x -> y");
|
||||
const svg = await d2.render(result.diagram, { noXMLTag: true });
|
||||
expect(svg).not.toContain('<?xml version="1.0"');
|
||||
await d2.worker.terminate();
|
||||
}, 20000);
|
||||
|
||||
test("force appendix works", async () => {
|
||||
const d2 = new D2();
|
||||
const result = await d2.compile("x: {tooltip: x appendix}", { forceAppendix: true });
|
||||
const svg = await d2.render(result.diagram, { forceAppendix: true });
|
||||
expect(svg).toContain("<svg");
|
||||
expect(svg).toContain("</svg>");
|
||||
expect(svg).toContain('class="appendix"');
|
||||
await d2.worker.terminate();
|
||||
}, 20000);
|
||||
|
||||
test("animated multi-board works", async () => {
|
||||
const d2 = new D2();
|
||||
const source = `
|
||||
x -> y
|
||||
layers: {
|
||||
numbers: {
|
||||
1 -> 2
|
||||
}
|
||||
}
|
||||
`;
|
||||
const options = { target: "*", animateInterval: 1000 };
|
||||
const result = await d2.compile(source, options);
|
||||
const svg = await d2.render(result.diagram, result.renderOptions);
|
||||
expect(svg).toContain("<svg");
|
||||
expect(svg).toContain("</svg>");
|
||||
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("unicode characters work", async () => {
|
||||
const d2 = new D2();
|
||||
const result = await d2.compile("こんにちは -> ♒️");
|
||||
const svg = await d2.render(result.diagram);
|
||||
expect(svg).toContain("<svg");
|
||||
expect(svg).toContain("</svg>");
|
||||
expect(svg).toContain("こんにちは");
|
||||
expect(svg).toContain("♒️");
|
||||
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);
|
||||
|
||||
test("handles unanimated multi-board error correctly", async () => {
|
||||
const d2 = new D2();
|
||||
const source = `
|
||||
x -> y
|
||||
layers: {
|
||||
numbers: {
|
||||
1 -> 2
|
||||
}
|
||||
}
|
||||
`;
|
||||
const result = await d2.compile(source);
|
||||
try {
|
||||
await d2.render(result.diagram, { target: "*" });
|
||||
throw new Error("Should have thrown compile error");
|
||||
} catch (err) {
|
||||
expect(err).toBeDefined();
|
||||
expect(err.message).not.toContain("Should have thrown compile error");
|
||||
}
|
||||
await d2.worker.terminate();
|
||||
}, 20000);
|
||||
|
||||
test("handles invalid imports correctly", async () => {
|
||||
const d2 = new D2();
|
||||
const fs = {
|
||||
"folder/index.d2": "a: @../invalid",
|
||||
"import.d2": "x: {shape: circle}",
|
||||
};
|
||||
const inputPath = "folder/index.d2";
|
||||
try {
|
||||
await d2.compile({ fs, inputPath });
|
||||
throw new Error("Should have thrown compile error");
|
||||
} catch (err) {
|
||||
expect(err).toBeDefined();
|
||||
expect(err.message).not.toContain("Should have thrown compile 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;
|
||||
};
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
|
@ -11,8 +11,6 @@ import (
|
|||
|
||||
"log/slog"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
|
||||
"oss.terrastruct.com/util-go/xdefer"
|
||||
|
||||
"oss.terrastruct.com/util-go/go2"
|
||||
|
|
@ -20,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"
|
||||
|
|
@ -80,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
|
||||
}
|
||||
|
||||
|
|
@ -136,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
|
||||
}
|
||||
|
||||
|
|
@ -183,11 +182,11 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
|
|||
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.Any("err", err))
|
||||
}
|
||||
|
|
@ -195,7 +194,7 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (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
|
||||
}
|
||||
|
|
@ -216,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
|
||||
}
|
||||
|
|
|
|||
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:
|
||||
|
|
|
|||
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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
@ -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{
|
||||
|
|
@ -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{
|
||||
|
|
|
|||
|
|
@ -466,7 +466,12 @@ func (sd *sequenceDiagram) placeNotes() {
|
|||
|
||||
for _, msg := range sd.messages {
|
||||
if sd.verticalIndices[msg.AbsID()] < verticalIndex {
|
||||
y += sd.yStep + float64(msg.LabelDimensions.Height)
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -77,6 +77,15 @@ func Compile(ctx context.Context, input string, compileOpts *CompileOptions, ren
|
|||
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -28,7 +28,7 @@ func GetRefRanges(path string, fs map[string]string, boardPath []string, key str
|
|||
var f *d2ir.Field
|
||||
if mk.Key != nil {
|
||||
for _, p := range mk.Key.Path {
|
||||
f = m.GetField(p.Unbox().ScalarString())
|
||||
f = m.GetField(p.Unbox())
|
||||
if f == nil {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
|
@ -92,3 +92,66 @@ func getBoardMap(path string, fs map[string]string, boardPath []string) (*d2ir.M
|
|||
}
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
package d2lsp_test
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"oss.terrastruct.com/d2/d2ast"
|
||||
"oss.terrastruct.com/d2/d2lsp"
|
||||
"oss.terrastruct.com/util-go/assert"
|
||||
)
|
||||
|
|
@ -117,6 +119,12 @@ hi
|
|||
layers: {
|
||||
x: {
|
||||
hello
|
||||
|
||||
layers: {
|
||||
y: {
|
||||
qwer
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
|
|
@ -130,6 +138,184 @@ layers: {
|
|||
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))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
156
d2oracle/edit.go
156
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
|
||||
|
|
@ -389,7 +398,7 @@ func _set(g *d2graph.Graph, baseAST *d2ast.Map, key string, tag, value *string)
|
|||
if baseAST != g.AST || imported {
|
||||
writeableRefs := GetWriteableRefs(obj, baseAST)
|
||||
for _, ref := range writeableRefs {
|
||||
if ref.MapKey != nil && ref.MapKey.Value.Map != nil {
|
||||
if ref.MapKey != nil && ref.MapKey.Value.Map != nil && ref.MapKey.Key == mk.Key {
|
||||
maybeNewScope = ref.MapKey.Value.Map
|
||||
}
|
||||
}
|
||||
|
|
@ -491,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"),
|
||||
}
|
||||
|
|
@ -941,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)
|
||||
|
|
@ -1756,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)
|
||||
|
|
@ -3304,3 +3324,135 @@ func filterReservedPath(path []*d2ast.StringBox) (filtered []*d2ast.StringBox) {
|
|||
}
|
||||
return
|
||||
}
|
||||
|
||||
func UpdateImport(dsl, path string, newPath *string) (_ string, err error) {
|
||||
if newPath == nil {
|
||||
defer xdefer.Errorf(&err, "failed to remove import %#v", path)
|
||||
} else {
|
||||
defer xdefer.Errorf(&err, "failed to update import from %#v to %#v", path, *newPath)
|
||||
}
|
||||
|
||||
ast, err := d2parser.Parse("", strings.NewReader(dsl), nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
_updateImport(ast, path, newPath)
|
||||
|
||||
return d2format.Format(ast), nil
|
||||
}
|
||||
|
||||
func _updateImport(m *d2ast.Map, oldPath string, newPath *string) {
|
||||
for i := 0; i < len(m.Nodes); i++ {
|
||||
node := m.Nodes[i]
|
||||
|
||||
if node.Import != nil {
|
||||
importPath := node.Import.PathWithPre()
|
||||
if matchesImportPath(importPath, oldPath) {
|
||||
if newPath == nil {
|
||||
if node.Import.Spread {
|
||||
m.Nodes = append(m.Nodes[:i], m.Nodes[i+1:]...)
|
||||
i--
|
||||
} else {
|
||||
node.Import = nil
|
||||
}
|
||||
} else {
|
||||
updateImportPath(node.Import, getNewImportPath(importPath, oldPath, *newPath))
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if node.MapKey != nil {
|
||||
if node.MapKey.Value.Import != nil {
|
||||
importPath := node.MapKey.Value.Import.PathWithPre()
|
||||
if matchesImportPath(importPath, oldPath) {
|
||||
if newPath == nil {
|
||||
if node.MapKey.Value.Import.Spread && node.MapKey.Value.Map == nil {
|
||||
m.Nodes = append(m.Nodes[:i], m.Nodes[i+1:]...)
|
||||
i--
|
||||
} else {
|
||||
node.MapKey.Value.Import = nil
|
||||
}
|
||||
} else {
|
||||
updateImportPath(node.MapKey.Value.Import, getNewImportPath(importPath, oldPath, *newPath))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
primaryImport := node.MapKey.Primary.Unbox()
|
||||
if primaryImport != nil {
|
||||
value, ok := primaryImport.(d2ast.Value)
|
||||
if ok {
|
||||
importBox := d2ast.MakeValueBox(value)
|
||||
if importBox.Import != nil {
|
||||
importPath := importBox.Import.PathWithPre()
|
||||
if matchesImportPath(importPath, oldPath) {
|
||||
if newPath == nil {
|
||||
node.MapKey.Primary = d2ast.ScalarBox{}
|
||||
} else {
|
||||
updateImportPath(importBox.Import, getNewImportPath(importPath, oldPath, *newPath))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if node.MapKey.Value.Map != nil {
|
||||
_updateImport(node.MapKey.Value.Map, oldPath, newPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateImportPath(imp *d2ast.Import, newPath string) {
|
||||
var pre string
|
||||
pathPart := newPath
|
||||
|
||||
for i, r := range newPath {
|
||||
if r != '.' && r != '/' {
|
||||
pre = newPath[:i]
|
||||
pathPart = newPath[i:]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if pre == "" && len(newPath) > 0 && (newPath[0] == '.' || newPath[0] == '/') {
|
||||
pre = newPath
|
||||
pathPart = ""
|
||||
}
|
||||
|
||||
imp.Pre = pre
|
||||
|
||||
if pathPart != "" {
|
||||
if len(imp.Path) > 0 {
|
||||
imp.Path[0] = d2ast.MakeValueBox(d2ast.RawString(pathPart, true)).StringBox()
|
||||
} else {
|
||||
imp.Path = []*d2ast.StringBox{
|
||||
d2ast.MakeValueBox(d2ast.RawString(pathPart, true)).StringBox(),
|
||||
}
|
||||
}
|
||||
} else if len(imp.Path) == 0 {
|
||||
imp.Path = []*d2ast.StringBox{
|
||||
d2ast.MakeValueBox(d2ast.RawString("", true)).StringBox(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func matchesImportPath(importPath, oldPath string) bool {
|
||||
isDir := strings.HasSuffix(oldPath, "/")
|
||||
if isDir {
|
||||
return strings.HasPrefix(importPath, oldPath)
|
||||
}
|
||||
return importPath == oldPath
|
||||
}
|
||||
|
||||
func getNewImportPath(importPath, oldPath, newPath string) string {
|
||||
isOldDir := strings.HasSuffix(oldPath, "/")
|
||||
isNewDir := strings.HasSuffix(newPath, "/")
|
||||
if isOldDir && isNewDir {
|
||||
relPath := importPath[len(oldPath):]
|
||||
return newPath + relPath
|
||||
}
|
||||
return newPath
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ func TestCreate(t *testing.T) {
|
|||
boardPath []string
|
||||
name string
|
||||
text string
|
||||
fsTexts map[string]string
|
||||
key string
|
||||
|
||||
expKey string
|
||||
|
|
@ -559,6 +560,37 @@ layers: {
|
|||
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
|
||||
|
|
@ -774,6 +806,35 @@ steps: {
|
|||
d 2
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "image-edge",
|
||||
|
||||
text: `...@k
|
||||
a.b: {
|
||||
icon: https://icons.terrastruct.com/essentials/004-picture.svg
|
||||
shape: image
|
||||
}
|
||||
`,
|
||||
fsTexts: map[string]string{
|
||||
"k.d2": `
|
||||
a: {
|
||||
b
|
||||
c
|
||||
}
|
||||
`,
|
||||
},
|
||||
key: `a.b -> a.c`,
|
||||
boardPath: []string{},
|
||||
|
||||
expKey: `a.(b -> c)[0]`,
|
||||
exp: `...@k
|
||||
a.b: {
|
||||
icon: https://icons.terrastruct.com/essentials/004-picture.svg
|
||||
shape: image
|
||||
}
|
||||
a.(b -> c)
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
|
@ -785,7 +846,8 @@ steps: {
|
|||
|
||||
var newKey string
|
||||
et := editTest{
|
||||
text: tc.text,
|
||||
text: tc.text,
|
||||
fsTexts: tc.fsTexts,
|
||||
testFunc: func(g *d2graph.Graph) (*d2graph.Graph, error) {
|
||||
var err error
|
||||
g, newKey, err = d2oracle.Create(g, tc.boardPath, tc.key)
|
||||
|
|
@ -1023,6 +1085,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
|
||||
`,
|
||||
},
|
||||
{
|
||||
|
|
@ -2426,6 +2498,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",
|
||||
|
||||
|
|
@ -2656,6 +2750,65 @@ scenarios: {
|
|||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
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
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "set-style-in-layer",
|
||||
text: `hey
|
||||
|
||||
layers: {
|
||||
k: {
|
||||
b: {style.stroke: "#969db4"}
|
||||
}
|
||||
}
|
||||
|
||||
layers: {
|
||||
x: {
|
||||
y
|
||||
}
|
||||
}
|
||||
`,
|
||||
boardPath: []string{"x"},
|
||||
key: `y.style.fill`,
|
||||
value: go2.Pointer(`#ff0000`),
|
||||
exp: `hey
|
||||
|
||||
layers: {
|
||||
k: {
|
||||
b: {style.stroke: "#969db4"}
|
||||
}
|
||||
}
|
||||
|
||||
layers: {
|
||||
x: {
|
||||
y: {style.fill: "#ff0000"}
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
|
@ -6027,6 +6180,28 @@ y
|
|||
exp: `y
|
||||
a -> b
|
||||
c -> d
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "underscore_linked",
|
||||
text: `k
|
||||
|
||||
layers: {
|
||||
x: {
|
||||
a
|
||||
b: {link: _}
|
||||
}
|
||||
}
|
||||
`,
|
||||
key: `b`,
|
||||
boardPath: []string{"x"},
|
||||
exp: `k
|
||||
|
||||
layers: {
|
||||
x: {
|
||||
a
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
|
|
@ -7989,6 +8164,32 @@ y
|
|||
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
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
|
@ -9396,3 +9597,288 @@ scenarios: {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateImport(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
boardPath []string
|
||||
text string
|
||||
fsTexts map[string]string
|
||||
path string
|
||||
newPath *string
|
||||
|
||||
expErr string
|
||||
exp string
|
||||
assertions func(t *testing.T, g *d2graph.Graph)
|
||||
}{
|
||||
{
|
||||
name: "remove_import",
|
||||
text: `x: @meow
|
||||
y
|
||||
`,
|
||||
path: "meow",
|
||||
newPath: nil,
|
||||
exp: `x
|
||||
y
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "remove_spread_import",
|
||||
text: `x
|
||||
...@meow
|
||||
y`,
|
||||
path: "meow",
|
||||
newPath: nil,
|
||||
exp: `x
|
||||
|
||||
y
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "update_import",
|
||||
text: `x: @meow
|
||||
y
|
||||
`,
|
||||
path: "meow",
|
||||
newPath: go2.Pointer("woof"),
|
||||
exp: `x: @woof
|
||||
y
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "update_import_with_dir",
|
||||
text: `x: @foo/meow
|
||||
y
|
||||
`,
|
||||
path: "foo/meow",
|
||||
newPath: go2.Pointer("bar/woof"),
|
||||
exp: `x: @bar/woof
|
||||
y
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "update_spread_import",
|
||||
text: `x
|
||||
...@meow
|
||||
y
|
||||
`,
|
||||
path: "meow",
|
||||
newPath: go2.Pointer("woof"),
|
||||
exp: `x
|
||||
...@woof
|
||||
y
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "no_matching_import",
|
||||
text: `x: @cat
|
||||
y
|
||||
`,
|
||||
path: "meow",
|
||||
newPath: go2.Pointer("woof"),
|
||||
exp: `x: @cat
|
||||
y
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "nested_import",
|
||||
text: `container: {
|
||||
x: @meow
|
||||
y
|
||||
}
|
||||
`,
|
||||
path: "meow",
|
||||
newPath: go2.Pointer("woof"),
|
||||
exp: `container: {
|
||||
x: @woof
|
||||
y
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "remove_nested_import",
|
||||
text: `container: {
|
||||
x: @meow
|
||||
y
|
||||
}
|
||||
`,
|
||||
path: "meow",
|
||||
newPath: nil,
|
||||
exp: `container: {
|
||||
x
|
||||
y
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "multiple_imports",
|
||||
text: `x: @meow
|
||||
y: @meow
|
||||
z
|
||||
`,
|
||||
path: "meow",
|
||||
newPath: go2.Pointer("woof"),
|
||||
exp: `x: @woof
|
||||
y: @woof
|
||||
z
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "mixed_imports",
|
||||
text: `x: @meow
|
||||
y
|
||||
...@meow
|
||||
z
|
||||
`,
|
||||
path: "meow",
|
||||
newPath: go2.Pointer("woof"),
|
||||
exp: `x: @woof
|
||||
y
|
||||
...@woof
|
||||
z
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "in_layer",
|
||||
text: `x
|
||||
|
||||
layers: {
|
||||
y: {
|
||||
z: @meow
|
||||
}
|
||||
}
|
||||
`,
|
||||
path: "meow",
|
||||
newPath: go2.Pointer("woof"),
|
||||
exp: `x
|
||||
|
||||
layers: {
|
||||
y: {
|
||||
z: @woof
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "layer_import",
|
||||
text: `x
|
||||
|
||||
layers: {
|
||||
y: {
|
||||
...@meow
|
||||
}
|
||||
}
|
||||
`,
|
||||
path: "meow",
|
||||
newPath: go2.Pointer("woof"),
|
||||
exp: `x
|
||||
|
||||
layers: {
|
||||
y: {
|
||||
...@woof
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "update_directory_import",
|
||||
text: `x: @foo/bar
|
||||
y: @foo/baz
|
||||
z
|
||||
`,
|
||||
path: "foo/",
|
||||
newPath: go2.Pointer("woof/"),
|
||||
exp: `x: @woof/bar
|
||||
y: @woof/baz
|
||||
z
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "remove_directory_import",
|
||||
text: `x: @foo/bar
|
||||
y: @foo/baz
|
||||
z
|
||||
`,
|
||||
path: "foo/",
|
||||
newPath: nil,
|
||||
exp: `x
|
||||
y
|
||||
z
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "update_deep_directory_paths",
|
||||
text: `x: @foo/bar/baz
|
||||
y: @foo/qux/quux
|
||||
z
|
||||
`,
|
||||
path: "foo/",
|
||||
newPath: go2.Pointer("woof/"),
|
||||
exp: `x: @woof/bar/baz
|
||||
y: @woof/qux/quux
|
||||
z
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "update_relative_import-1",
|
||||
text: `x: @../meow
|
||||
y
|
||||
`,
|
||||
path: "../meow",
|
||||
newPath: go2.Pointer("../woof"),
|
||||
exp: `x: @../woof
|
||||
y
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "update_relative_import-2",
|
||||
text: `x: @../meow
|
||||
y
|
||||
`,
|
||||
path: "../meow",
|
||||
newPath: go2.Pointer("woof"),
|
||||
exp: `x: @woof
|
||||
y
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "update_relative_import-3",
|
||||
text: `x: @../meow
|
||||
y
|
||||
`,
|
||||
path: "../meow",
|
||||
newPath: go2.Pointer("../meow/woof"),
|
||||
exp: `x: @../meow/woof
|
||||
y
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "update_relative_import-4",
|
||||
text: `x: @../meow
|
||||
y
|
||||
`,
|
||||
path: "../meow",
|
||||
newPath: go2.Pointer("../g/woof"),
|
||||
exp: `x: @../g/woof
|
||||
y
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
got, err := d2oracle.UpdateImport(tc.text, tc.path, tc.newPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != tc.exp {
|
||||
t.Fatalf("tc.exp != newText:\n%s", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,51 +48,35 @@ func ReplaceBoardNode(ast, ast2 *d2ast.Map, boardPath []string) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
findMap := func(root *d2ast.Map, name string) *d2ast.Map {
|
||||
for _, n := range root.Nodes {
|
||||
if n.MapKey != nil && n.MapKey.Key != nil && n.MapKey.Key.Path[0].Unbox().ScalarString() == name {
|
||||
return n.MapKey.Value.Map
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return replaceBoardNodeInMap(ast, ast2, boardPath, "layers") ||
|
||||
replaceBoardNodeInMap(ast, ast2, boardPath, "scenarios") ||
|
||||
replaceBoardNodeInMap(ast, ast2, boardPath, "steps")
|
||||
}
|
||||
|
||||
layersMap := findMap(ast, "layers")
|
||||
scenariosMap := findMap(ast, "scenarios")
|
||||
stepsMap := findMap(ast, "steps")
|
||||
func replaceBoardNodeInMap(ast, ast2 *d2ast.Map, boardPath []string, boardType string) bool {
|
||||
var matches []*d2ast.Map
|
||||
|
||||
if layersMap != nil {
|
||||
m := findMap(layersMap, boardPath[0])
|
||||
if m != nil {
|
||||
if len(boardPath) > 1 {
|
||||
return ReplaceBoardNode(m, ast2, boardPath[1:])
|
||||
} else {
|
||||
m.Nodes = ast2.Nodes
|
||||
return true
|
||||
}
|
||||
for _, n := range ast.Nodes {
|
||||
if n.MapKey != nil && n.MapKey.Key != nil &&
|
||||
n.MapKey.Key.Path[0].Unbox().ScalarString() == boardType &&
|
||||
n.MapKey.Value.Map != nil {
|
||||
matches = append(matches, n.MapKey.Value.Map)
|
||||
}
|
||||
}
|
||||
|
||||
if scenariosMap != nil {
|
||||
m := findMap(scenariosMap, boardPath[0])
|
||||
if m != nil {
|
||||
if len(boardPath) > 1 {
|
||||
return ReplaceBoardNode(m, ast2, boardPath[1:])
|
||||
} else {
|
||||
m.Nodes = ast2.Nodes
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if stepsMap != nil {
|
||||
m := findMap(stepsMap, boardPath[0])
|
||||
if m != nil {
|
||||
if len(boardPath) > 1 {
|
||||
return ReplaceBoardNode(m, ast2, boardPath[1:])
|
||||
} else {
|
||||
m.Nodes = ast2.Nodes
|
||||
return true
|
||||
for _, boardMap := range matches {
|
||||
for _, n := range boardMap.Nodes {
|
||||
if n.MapKey != nil && n.MapKey.Key != nil &&
|
||||
n.MapKey.Key.Path[0].Unbox().ScalarString() == boardPath[0] &&
|
||||
n.MapKey.Value.Map != nil {
|
||||
if len(boardPath) > 1 {
|
||||
if ReplaceBoardNode(n.MapKey.Value.Map, ast2, boardPath[1:]) {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
n.MapKey.Value.Map.Nodes = ast2.Nodes
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -249,7 +233,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])
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -984,6 +984,14 @@ func (p *parser) parseKey() (k *d2ast.KeyPath) {
|
|||
k = nil
|
||||
} else {
|
||||
k.Range.End = k.Path[len(k.Path)-1].Unbox().GetRange().End
|
||||
for _, part := range k.Path {
|
||||
if part.Unbox() != nil {
|
||||
if len(part.Unbox().ScalarString()) > 518 {
|
||||
p.errorf(k.Range.Start, k.Range.End, "key length %d exceeds maximum allowed length of 518", len(part.Unbox().ScalarString()))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
|
|
@ -1668,6 +1676,20 @@ func (p *parser) parseValue() d2ast.ValueBox {
|
|||
}
|
||||
return box
|
||||
}
|
||||
if strings.EqualFold(s.ScalarString(), "suspend") {
|
||||
box.Suspension = &d2ast.Suspension{
|
||||
Range: s.Range,
|
||||
Value: true,
|
||||
}
|
||||
return box
|
||||
}
|
||||
if strings.EqualFold(s.ScalarString(), "unsuspend") {
|
||||
box.Suspension = &d2ast.Suspension{
|
||||
Range: s.Range,
|
||||
Value: false,
|
||||
}
|
||||
return box
|
||||
}
|
||||
|
||||
if strings.EqualFold(s.ScalarString(), "true") {
|
||||
box.Boolean = &d2ast.Boolean{
|
||||
|
|
|
|||
|
|
@ -500,6 +500,15 @@ func testImport(t *testing.T) {
|
|||
assert.ErrorString(t, err, "d2/testdata/d2parser/TestParse/import/#09.d2:1:7: unquoted strings cannot begin with ...@ as that's import spread syntax")
|
||||
},
|
||||
},
|
||||
{
|
||||
text: `gcloud: {
|
||||
icon: data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMwAAADACAMAAAB/Pny7AAAAjVBMVEVChfT///89fOM3gPTk7P0+g/R0oPaKr+0rc+E0d+I5euNYjObt9P1lmiVBORw0KGgoAAAANSUhEUgAAAiVBORw0KGgoAAAANSUhEUgAAAiVBORw0KGgoAAAANSUhEUgAAABORw0KGgoAAAANSUhEUgAAAMwAAADACAMAAAB/Pny7AAAAjVBMVEVChfT///89fOM3gPTk7P0+g/R0oPaKr+0rc+E0d+I5euNYjObt9P1lmiVBORw0KGgoAAAANSUhEUgAAAiVBORw0KGgoAAAANSUhEUgAAAiVBORw0KGgoAAAANSUhEUgAAABORw0KGgoAAAANSUhEUgAAAMwAAADACAMAAAB/Pny7AAAAjVBMVEVChfT///89fOM3gPTk7P0+g/R0oPaKr+0rc+E0d+I5euNYjObt9P1lmiVBORw0KGgoAAAANSUhEUgAAAiVBORw0KGgoAAAANSUhEUgAAAiVBORw0KGgoAAAANSUhEUgAAA
|
||||
}
|
||||
`,
|
||||
assert: func(t testing.TB, ast *d2ast.Map, err error) {
|
||||
assert.ErrorString(t, err, "d2/testdata/d2parser/TestParse/import/#10.d2:2:24: key length 555 exceeds maximum allowed length of 518")
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
runa(t, tca)
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import (
|
|||
"context"
|
||||
"encoding/json"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"oss.terrastruct.com/util-go/xexec"
|
||||
"oss.terrastruct.com/util-go/xmain"
|
||||
|
|
@ -170,7 +171,7 @@ func FindPlugin(ctx context.Context, ps []Plugin, name string) (Plugin, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if info.Name == name {
|
||||
if strings.EqualFold(info.Name, name) {
|
||||
return p, nil
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ func Wrap(rootDiagram *d2target.Diagram, svgs [][]byte, renderOpts d2svg.RenderO
|
|||
)
|
||||
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)
|
||||
|
||||
|
|
@ -77,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
|
||||
}
|
||||
|
|
@ -102,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",
|
||||
|
|
@ -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",
|
||||
|
|
@ -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",
|
||||
|
|
@ -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",
|
||||
|
|
@ -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", {
|
||||
|
|
@ -320,7 +310,7 @@ 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) {
|
||||
func Connection(r jsrunner.JSRunner, connection d2target.Connection, path, attrs string) (string, error) {
|
||||
animatedClass := ""
|
||||
if connection.Animated {
|
||||
animatedClass = " animated-connection"
|
||||
|
|
@ -388,7 +378,7 @@ func Connection(r *Runner, connection d2target.Connection, path, attrs string) (
|
|||
}
|
||||
|
||||
// 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",
|
||||
|
|
@ -530,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",
|
||||
|
|
@ -681,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)
|
||||
|
|
@ -692,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)
|
||||
|
|
@ -722,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
|
||||
}
|
||||
|
|
@ -756,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:
|
||||
|
|
@ -857,11 +847,27 @@ func ArrowheadJS(r *Runner, arrowhead d2target.Arrowhead, stroke string, strokeW
|
|||
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 {
|
||||
|
|
|
|||
|
|
@ -462,6 +462,20 @@ a.9 <-> b.9: cf-one-required {
|
|||
source-arrowhead.shape: cf-one-required
|
||||
target-arrowhead.shape: cf-one-required
|
||||
}
|
||||
a.10 <-> b.10: box {
|
||||
source-arrowhead.shape: box
|
||||
target-arrowhead.shape: box
|
||||
}
|
||||
a.11 <-> b.11: box-filled {
|
||||
source-arrowhead: {
|
||||
shape: box
|
||||
style.filled: true
|
||||
}
|
||||
target-arrowhead: {
|
||||
shape: box
|
||||
style.filled: true
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
|
|
@ -1343,6 +1357,14 @@ item -> customer: is(Adult)
|
|||
customer -> item: true
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "test-gradient-fill-values-in-sketch-mode",
|
||||
script: `
|
||||
x->y
|
||||
x.style.fill: "linear-gradient(#000000,#ffffff)"
|
||||
y.style.fill: "linear-gradient(#ffffff,#000000)"
|
||||
`,
|
||||
},
|
||||
}
|
||||
runa(t, tcs)
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue