Compare commits
1 commit
master
...
reuse-inst
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
45e3d41b75 |
2
.github/workflows/ci.yml
vendored
|
|
@ -17,7 +17,7 @@ jobs:
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets._GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets._GITHUB_TOKEN }}
|
||||||
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v3
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
||||||
name: d2chaos
|
name: d2chaos
|
||||||
|
|
|
||||||
36
.github/workflows/daily.yml
vendored
|
|
@ -8,41 +8,7 @@ concurrency:
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
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:
|
ci:
|
||||||
needs: [npm-nightly]
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|
@ -54,7 +20,7 @@ jobs:
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets._GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets._GITHUB_TOKEN }}
|
||||||
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v3
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
||||||
name: d2chaos
|
name: d2chaos
|
||||||
|
|
|
||||||
2
Makefile
|
|
@ -23,4 +23,4 @@ race: fmt
|
||||||
prefix "$@" ./ci/test.sh --race ./...
|
prefix "$@" ./ci/test.sh --race ./...
|
||||||
.PHONY: js
|
.PHONY: js
|
||||||
js: gen
|
js: gen
|
||||||
cd d2js/js && NPM_VERSION="${NPM_VERSION}" prefix "$@" ./make.sh all
|
cd d2js/js && prefix "$@" ./make.sh all
|
||||||
|
|
|
||||||
13
README.md
|
|
@ -4,13 +4,11 @@
|
||||||
A modern diagram scripting language that turns text to diagrams.
|
A modern diagram scripting language that turns text to diagrams.
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
[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)
|
[Docs](https://d2lang.com) | [Cheat sheet](./docs/assets/cheat_sheet.pdf) | [Comparisons](https://text-to-diagram.com) | [Playground](https://play.d2lang.com)
|
||||||
|
|
||||||
[](https://github.com/terrastruct/d2/actions/workflows/ci.yml)
|
[](https://github.com/terrastruct/d2/actions/workflows/ci.yml)
|
||||||
[](https://github.com/terrastruct/d2/actions/workflows/daily.yml)
|
[](https://github.com/terrastruct/d2/actions/workflows/daily.yml)
|
||||||
[](https://github.com/terrastruct/d2/releases)
|
[](https://github.com/terrastruct/d2/releases)
|
||||||
[](./CHANGELOG.md)
|
|
||||||
[](https://www.npmjs.com/package/@terrastruct/d2)
|
|
||||||
[](https://discord.gg/NF6X8K4eDq)
|
[](https://discord.gg/NF6X8K4eDq)
|
||||||
[](https://twitter.com/terrastruct)
|
[](https://twitter.com/terrastruct)
|
||||||
[](./LICENSE.txt)
|
[](./LICENSE.txt)
|
||||||
|
|
@ -18,9 +16,6 @@
|
||||||
<a href="https://play.d2lang.com">
|
<a href="https://play.d2lang.com">
|
||||||
<img src="./docs/assets/playground_button.png" alt="D2 Playground button" width="200" />
|
<img src="./docs/assets/playground_button.png" alt="D2 Playground button" width="200" />
|
||||||
</a>
|
</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
|
https://user-images.githubusercontent.com/3120367/206125010-bd1fea8e-248a-43e7-8f85-0bbfca0c6e2a.mp4
|
||||||
|
|
||||||
|
|
@ -243,7 +238,7 @@ let us know and we'll be happy to include it here!
|
||||||
|
|
||||||
### Community plugins
|
### Community plugins
|
||||||
|
|
||||||
- **Tree-sitter grammar**: [https://github.com/ravsii/tree-sitter-d2](https://github.com/ravsii/tree-sitter-d2)
|
- **Tree-sitter grammar**: [https://git.pleshevski.ru/pleshevskiy/tree-sitter-d2](https://git.pleshevski.ru/pleshevskiy/tree-sitter-d2)
|
||||||
- **Emacs major mode**: [https://github.com/andorsk/d2-mode](https://github.com/andorsk/d2-mode)
|
- **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)
|
- **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)
|
- **Telegram bot**: [https://github.com/meinside/telegram-d2-bot](https://github.com/meinside/telegram-d2-bot)
|
||||||
|
|
@ -266,16 +261,12 @@ let us know and we'll be happy to include it here!
|
||||||
- **ent2d2**: [https://github.com/tmc/ent2d2](https://github.com/tmc/ent2d2)
|
- **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)
|
- **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)
|
- **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)
|
- **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
|
### Misc
|
||||||
|
|
||||||
- **Comparison site**: [https://github.com/terrastruct/text-to-diagram-site](https://github.com/terrastruct/text-to-diagram-site)
|
- **Comparison site**: [https://github.com/terrastruct/text-to-diagram-site](https://github.com/terrastruct/text-to-diagram-site)
|
||||||
- **Playground**: [https://github.com/terrastruct/d2-playground](https://github.com/terrastruct/d2-playground)
|
- **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)
|
- **Language docs**: [https://github.com/terrastruct/d2-docs](https://github.com/terrastruct/d2-docs)
|
||||||
- **Hosted icons**: [https://icons.terrastruct.com](https://icons.terrastruct.com)
|
- **Hosted icons**: [https://icons.terrastruct.com](https://icons.terrastruct.com)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ cd -- "$(dirname "$0")/../.."
|
||||||
. ./ci/sub/lib.sh
|
. ./ci/sub/lib.sh
|
||||||
|
|
||||||
tag="$(sh_c docker build \
|
tag="$(sh_c docker build \
|
||||||
--build-arg GOVERSION="1.23.6.linux-$ARCH" \
|
--build-arg GOVERSION="1.22.2.linux-$ARCH" \
|
||||||
-qf ./ci/release/linux/Dockerfile ./ci/release/linux)"
|
-qf ./ci/release/linux/Dockerfile ./ci/release/linux)"
|
||||||
docker_run \
|
docker_run \
|
||||||
-e DRY_RUN \
|
-e DRY_RUN \
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,24 @@
|
||||||
#### Features 🚀
|
#### Features 🚀
|
||||||
|
|
||||||
- `cross` arrowhead shape is available [#2190](https://github.com/terrastruct/d2/pull/2190)
|
- 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)
|
||||||
|
- `d2 fmt` now supports a `--check` flag [#2253](https://github.com/terrastruct/d2/pull/2253)
|
||||||
|
|
||||||
#### Improvements 🧹
|
#### 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)
|
||||||
|
|
||||||
#### Bugfixes ⛑️
|
#### 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)
|
||||||
For the latest d2.js changes, see separate [changelog](https://github.com/terrastruct/d2/blob/master/d2js/js/CHANGELOG.md).
|
- 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)
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,3 @@
|
||||||
#### Improvements 🧹
|
#### Improvements 🧹
|
||||||
|
|
||||||
#### Bugfixes ⛑️
|
#### Bugfixes ⛑️
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
For the latest d2.js changes, see separate [changelog](https://github.com/terrastruct/d2/blob/master/d2js/js/CHANGELOG.md).
|
|
||||||
|
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
#### 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)
|
|
||||||
|
|
@ -1,59 +0,0 @@
|
||||||
#### 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).
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
#!/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,6 +1,5 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
set -eu
|
set -eu
|
||||||
cd -- "$(dirname "$0")/../.."
|
cd -- "$(dirname "$0")/../.."
|
||||||
. "./ci/sub/lib.sh"
|
|
||||||
|
|
||||||
./ci/sub/release/release.sh "$@"
|
./ci/sub/release/release.sh "$@"
|
||||||
|
|
|
||||||
|
|
@ -8,17 +8,12 @@
|
||||||
.Nm d2
|
.Nm d2
|
||||||
.Op Fl -watch Ar false
|
.Op Fl -watch Ar false
|
||||||
.Op Fl -theme Em 0
|
.Op Fl -theme Em 0
|
||||||
.Op Fl -salt Ar string
|
|
||||||
.Ar file.d2
|
.Ar file.d2
|
||||||
.Op Ar file.svg | file.png
|
.Op Ar file.svg | file.png
|
||||||
.Nm d2
|
.Nm d2
|
||||||
.Ar layout Op Ar name
|
.Ar layout Op Ar name
|
||||||
.Nm d2
|
.Nm d2
|
||||||
.Ar fmt Ar file.d2 ...
|
.Ar fmt Ar file.d2 ...
|
||||||
.Nm d2
|
|
||||||
.Ar play Ar file.d2
|
|
||||||
.Nm d2
|
|
||||||
.Ar validate Ar file.d2
|
|
||||||
.Sh DESCRIPTION
|
.Sh DESCRIPTION
|
||||||
.Nm
|
.Nm
|
||||||
compiles and renders
|
compiles and renders
|
||||||
|
|
@ -133,24 +128,12 @@ The maximum number of seconds that D2 runs for before timing out and exiting. Wh
|
||||||
.It Fl -check Ar false
|
.It Fl -check Ar false
|
||||||
Check that the specified files are formatted correctly
|
Check that the specified files are formatted correctly
|
||||||
.Ns .
|
.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
|
.It Fl h , -help
|
||||||
Print usage information and exit
|
Print usage information and exit
|
||||||
.Ns .
|
.Ns .
|
||||||
.It Fl v , -version
|
.It Fl v , -version
|
||||||
Print version information and exit
|
Print version information and exit
|
||||||
.Ns .
|
.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
|
.El
|
||||||
.Sh SUBCOMMANDS
|
.Sh SUBCOMMANDS
|
||||||
.Bl -tag -width Fl
|
.Bl -tag -width Fl
|
||||||
|
|
@ -165,10 +148,7 @@ Lists available themes
|
||||||
.Ns .
|
.Ns .
|
||||||
.It Ar fmt Ar file.d2 ...
|
.It Ar fmt Ar file.d2 ...
|
||||||
Format all passed files
|
Format all passed files
|
||||||
.It Ar play Ar file.d2
|
.Ns .
|
||||||
Opens the file in playground, an online web viewer (https://play.d2lang.com)
|
|
||||||
.It Ar validate Ar file.d2
|
|
||||||
Validates file.d2
|
|
||||||
.El
|
.El
|
||||||
.Sh ENVIRONMENT VARIABLES
|
.Sh ENVIRONMENT VARIABLES
|
||||||
Many flags can also be set with environment variables.
|
Many flags can also be set with environment variables.
|
||||||
|
|
@ -217,12 +197,6 @@ See -h[ost] flag.
|
||||||
See -p[ort] flag.
|
See -p[ort] flag.
|
||||||
.It Ev Sy BROWSER
|
.It Ev Sy BROWSER
|
||||||
See --browser flag.
|
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
|
.El
|
||||||
.Sh SEE ALSO
|
.Sh SEE ALSO
|
||||||
.Xr d2plugin-tala 1
|
.Xr d2plugin-tala 1
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,6 @@ var _ Node = &Comment{}
|
||||||
var _ Node = &BlockComment{}
|
var _ Node = &BlockComment{}
|
||||||
|
|
||||||
var _ Node = &Null{}
|
var _ Node = &Null{}
|
||||||
var _ Node = &Suspension{}
|
|
||||||
var _ Node = &Boolean{}
|
var _ Node = &Boolean{}
|
||||||
var _ Node = &Number{}
|
var _ Node = &Number{}
|
||||||
var _ Node = &UnquotedString{}
|
var _ Node = &UnquotedString{}
|
||||||
|
|
@ -330,7 +329,6 @@ type Scalar interface {
|
||||||
|
|
||||||
// See String for rest.
|
// See String for rest.
|
||||||
var _ Scalar = &Null{}
|
var _ Scalar = &Null{}
|
||||||
var _ Scalar = &Suspension{}
|
|
||||||
var _ Scalar = &Boolean{}
|
var _ Scalar = &Boolean{}
|
||||||
var _ Scalar = &Number{}
|
var _ Scalar = &Number{}
|
||||||
|
|
||||||
|
|
@ -351,7 +349,6 @@ var _ String = &BlockString{}
|
||||||
func (c *Comment) node() {}
|
func (c *Comment) node() {}
|
||||||
func (c *BlockComment) node() {}
|
func (c *BlockComment) node() {}
|
||||||
func (n *Null) node() {}
|
func (n *Null) node() {}
|
||||||
func (n *Suspension) node() {}
|
|
||||||
func (b *Boolean) node() {}
|
func (b *Boolean) node() {}
|
||||||
func (n *Number) node() {}
|
func (n *Number) node() {}
|
||||||
func (s *UnquotedString) node() {}
|
func (s *UnquotedString) node() {}
|
||||||
|
|
@ -370,7 +367,6 @@ func (i *EdgeIndex) node() {}
|
||||||
func (c *Comment) Type() string { return "comment" }
|
func (c *Comment) Type() string { return "comment" }
|
||||||
func (c *BlockComment) Type() string { return "block comment" }
|
func (c *BlockComment) Type() string { return "block comment" }
|
||||||
func (n *Null) Type() string { return "null" }
|
func (n *Null) Type() string { return "null" }
|
||||||
func (n *Suspension) Type() string { return "suspension" }
|
|
||||||
func (b *Boolean) Type() string { return "boolean" }
|
func (b *Boolean) Type() string { return "boolean" }
|
||||||
func (n *Number) Type() string { return "number" }
|
func (n *Number) Type() string { return "number" }
|
||||||
func (s *UnquotedString) Type() string { return "unquoted string" }
|
func (s *UnquotedString) Type() string { return "unquoted string" }
|
||||||
|
|
@ -389,7 +385,6 @@ func (i *EdgeIndex) Type() string { return "edge index" }
|
||||||
func (c *Comment) GetRange() Range { return c.Range }
|
func (c *Comment) GetRange() Range { return c.Range }
|
||||||
func (c *BlockComment) GetRange() Range { return c.Range }
|
func (c *BlockComment) GetRange() Range { return c.Range }
|
||||||
func (n *Null) GetRange() Range { return n.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 (b *Boolean) GetRange() Range { return b.Range }
|
||||||
func (n *Number) GetRange() Range { return n.Range }
|
func (n *Number) GetRange() Range { return n.Range }
|
||||||
func (s *UnquotedString) GetRange() Range { return s.Range }
|
func (s *UnquotedString) GetRange() Range { return s.Range }
|
||||||
|
|
@ -414,7 +409,6 @@ func (i *Import) mapNode() {}
|
||||||
func (c *Comment) arrayNode() {}
|
func (c *Comment) arrayNode() {}
|
||||||
func (c *BlockComment) arrayNode() {}
|
func (c *BlockComment) arrayNode() {}
|
||||||
func (n *Null) arrayNode() {}
|
func (n *Null) arrayNode() {}
|
||||||
func (n *Suspension) arrayNode() {}
|
|
||||||
func (b *Boolean) arrayNode() {}
|
func (b *Boolean) arrayNode() {}
|
||||||
func (n *Number) arrayNode() {}
|
func (n *Number) arrayNode() {}
|
||||||
func (s *UnquotedString) arrayNode() {}
|
func (s *UnquotedString) arrayNode() {}
|
||||||
|
|
@ -427,7 +421,6 @@ func (a *Array) arrayNode() {}
|
||||||
func (m *Map) arrayNode() {}
|
func (m *Map) arrayNode() {}
|
||||||
|
|
||||||
func (n *Null) value() {}
|
func (n *Null) value() {}
|
||||||
func (n *Suspension) value() {}
|
|
||||||
func (b *Boolean) value() {}
|
func (b *Boolean) value() {}
|
||||||
func (n *Number) value() {}
|
func (n *Number) value() {}
|
||||||
func (s *UnquotedString) value() {}
|
func (s *UnquotedString) value() {}
|
||||||
|
|
@ -439,7 +432,6 @@ func (m *Map) value() {}
|
||||||
func (i *Import) value() {}
|
func (i *Import) value() {}
|
||||||
|
|
||||||
func (n *Null) scalar() {}
|
func (n *Null) scalar() {}
|
||||||
func (n *Suspension) scalar() {}
|
|
||||||
func (b *Boolean) scalar() {}
|
func (b *Boolean) scalar() {}
|
||||||
func (n *Number) scalar() {}
|
func (n *Number) scalar() {}
|
||||||
func (s *UnquotedString) scalar() {}
|
func (s *UnquotedString) scalar() {}
|
||||||
|
|
@ -450,7 +442,6 @@ func (s *BlockString) scalar() {}
|
||||||
func (c *Comment) Children() []Node { return nil }
|
func (c *Comment) Children() []Node { return nil }
|
||||||
func (c *BlockComment) Children() []Node { return nil }
|
func (c *BlockComment) Children() []Node { return nil }
|
||||||
func (n *Null) 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 (b *Boolean) Children() []Node { return nil }
|
||||||
func (n *Number) Children() []Node { return nil }
|
func (n *Number) Children() []Node { return nil }
|
||||||
func (s *SingleQuotedString) Children() []Node { return nil }
|
func (s *SingleQuotedString) Children() []Node { return nil }
|
||||||
|
|
@ -583,7 +574,6 @@ func Walk(node Node, fn func(Node) bool) {
|
||||||
|
|
||||||
// TODO: mistake, move into parse.go
|
// TODO: mistake, move into parse.go
|
||||||
func (n *Null) ScalarString() string { return "" }
|
func (n *Null) ScalarString() string { return "" }
|
||||||
func (n *Suspension) ScalarString() string { return "" }
|
|
||||||
func (b *Boolean) ScalarString() string { return strconv.FormatBool(b.Value) }
|
func (b *Boolean) ScalarString() string { return strconv.FormatBool(b.Value) }
|
||||||
func (n *Number) ScalarString() string { return n.Raw }
|
func (n *Number) ScalarString() string { return n.Raw }
|
||||||
func (s *UnquotedString) ScalarString() string {
|
func (s *UnquotedString) ScalarString() string {
|
||||||
|
|
@ -641,11 +631,6 @@ type Null struct {
|
||||||
Range Range `json:"range"`
|
Range Range `json:"range"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Suspension struct {
|
|
||||||
Range Range `json:"range"`
|
|
||||||
Value bool `json:"value"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Boolean struct {
|
type Boolean struct {
|
||||||
Range Range `json:"range"`
|
Range Range `json:"range"`
|
||||||
Value bool `json:"value"`
|
Value bool `json:"value"`
|
||||||
|
|
@ -1384,7 +1369,6 @@ func (ab ArrayNodeBox) Unbox() ArrayNode {
|
||||||
// ValueBox is used to box Value for JSON persistence.
|
// ValueBox is used to box Value for JSON persistence.
|
||||||
type ValueBox struct {
|
type ValueBox struct {
|
||||||
Null *Null `json:"null,omitempty"`
|
Null *Null `json:"null,omitempty"`
|
||||||
Suspension *Suspension `json:"suspension,omitempty"`
|
|
||||||
Boolean *Boolean `json:"boolean,omitempty"`
|
Boolean *Boolean `json:"boolean,omitempty"`
|
||||||
Number *Number `json:"number,omitempty"`
|
Number *Number `json:"number,omitempty"`
|
||||||
UnquotedString *UnquotedString `json:"unquoted_string,omitempty"`
|
UnquotedString *UnquotedString `json:"unquoted_string,omitempty"`
|
||||||
|
|
@ -1400,8 +1384,6 @@ func (vb ValueBox) Unbox() Value {
|
||||||
switch {
|
switch {
|
||||||
case vb.Null != nil:
|
case vb.Null != nil:
|
||||||
return vb.Null
|
return vb.Null
|
||||||
case vb.Suspension != nil:
|
|
||||||
return vb.Suspension
|
|
||||||
case vb.Boolean != nil:
|
case vb.Boolean != nil:
|
||||||
return vb.Boolean
|
return vb.Boolean
|
||||||
case vb.Number != nil:
|
case vb.Number != nil:
|
||||||
|
|
@ -1430,8 +1412,6 @@ func MakeValueBox(v Value) ValueBox {
|
||||||
switch v := v.(type) {
|
switch v := v.(type) {
|
||||||
case *Null:
|
case *Null:
|
||||||
vb.Null = v
|
vb.Null = v
|
||||||
case *Suspension:
|
|
||||||
vb.Suspension = v
|
|
||||||
case *Boolean:
|
case *Boolean:
|
||||||
vb.Boolean = v
|
vb.Boolean = v
|
||||||
case *Number:
|
case *Number:
|
||||||
|
|
@ -1457,7 +1437,6 @@ func MakeValueBox(v Value) ValueBox {
|
||||||
func (vb ValueBox) ScalarBox() ScalarBox {
|
func (vb ValueBox) ScalarBox() ScalarBox {
|
||||||
var sb ScalarBox
|
var sb ScalarBox
|
||||||
sb.Null = vb.Null
|
sb.Null = vb.Null
|
||||||
sb.Suspension = vb.Suspension
|
|
||||||
sb.Boolean = vb.Boolean
|
sb.Boolean = vb.Boolean
|
||||||
sb.Number = vb.Number
|
sb.Number = vb.Number
|
||||||
sb.UnquotedString = vb.UnquotedString
|
sb.UnquotedString = vb.UnquotedString
|
||||||
|
|
@ -1480,7 +1459,6 @@ func (vb ValueBox) StringBox() *StringBox {
|
||||||
// TODO: implement ScalarString()
|
// TODO: implement ScalarString()
|
||||||
type ScalarBox struct {
|
type ScalarBox struct {
|
||||||
Null *Null `json:"null,omitempty"`
|
Null *Null `json:"null,omitempty"`
|
||||||
Suspension *Suspension `json:"suspension,omitempty"`
|
|
||||||
Boolean *Boolean `json:"boolean,omitempty"`
|
Boolean *Boolean `json:"boolean,omitempty"`
|
||||||
Number *Number `json:"number,omitempty"`
|
Number *Number `json:"number,omitempty"`
|
||||||
UnquotedString *UnquotedString `json:"unquoted_string,omitempty"`
|
UnquotedString *UnquotedString `json:"unquoted_string,omitempty"`
|
||||||
|
|
@ -1493,8 +1471,6 @@ func (sb ScalarBox) Unbox() Scalar {
|
||||||
switch {
|
switch {
|
||||||
case sb.Null != nil:
|
case sb.Null != nil:
|
||||||
return sb.Null
|
return sb.Null
|
||||||
case sb.Suspension != nil:
|
|
||||||
return sb.Suspension
|
|
||||||
case sb.Boolean != nil:
|
case sb.Boolean != nil:
|
||||||
return sb.Boolean
|
return sb.Boolean
|
||||||
case sb.Number != nil:
|
case sb.Number != nil:
|
||||||
|
|
@ -1583,7 +1559,7 @@ func RawString(s string, inKey bool) String {
|
||||||
return &SingleQuotedString{Value: s}
|
return &SingleQuotedString{Value: s}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if s == "null" || s == "suspend" || s == "unsuspend" || strings.ContainsAny(s, UnquotedValueSpecials) {
|
} else if s == "null" || strings.ContainsAny(s, UnquotedValueSpecials) {
|
||||||
if !strings.ContainsRune(s, '"') && !strings.ContainsRune(s, '$') {
|
if !strings.ContainsRune(s, '"') && !strings.ContainsRune(s, '$') {
|
||||||
return FlatDoubleQuotedString(s)
|
return FlatDoubleQuotedString(s)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package d2chaos_test
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
|
|
@ -89,14 +90,14 @@ func TestD2Chaos(t *testing.T) {
|
||||||
|
|
||||||
func test(t *testing.T, textPath, text string) {
|
func test(t *testing.T, textPath, text string) {
|
||||||
t.Logf("writing d2 to %v (%d bytes)", textPath, len(text))
|
t.Logf("writing d2 to %v (%d bytes)", textPath, len(text))
|
||||||
err := os.WriteFile(textPath, []byte(text), 0644)
|
err := ioutil.WriteFile(textPath, []byte(text), 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
goencText := fmt.Sprintf("%#v", text)
|
goencText := fmt.Sprintf("%#v", text)
|
||||||
t.Logf("writing d2.goenc to %v (%d bytes)", textPath+".goenc", len(goencText))
|
t.Logf("writing d2.goenc to %v (%d bytes)", textPath+".goenc", len(goencText))
|
||||||
err = os.WriteFile(textPath+".goenc", []byte(goencText), 0644)
|
err = ioutil.WriteFile(textPath+".goenc", []byte(goencText), 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
package d2cli
|
package d2cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type exportExtension string
|
type exportExtension string
|
||||||
|
|
@ -16,24 +14,6 @@ const SVG exportExtension = ".svg"
|
||||||
|
|
||||||
var SUPPORTED_EXTENSIONS = []exportExtension{SVG, PNG, PDF, PPTX, GIF}
|
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 {
|
func getExportExtension(outputPath string) exportExtension {
|
||||||
ext := filepath.Ext(outputPath)
|
ext := filepath.Ext(outputPath)
|
||||||
for _, kext := range SUPPORTED_EXTENSIONS {
|
for _, kext := range SUPPORTED_EXTENSIONS {
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ import (
|
||||||
|
|
||||||
func TestOutputFormat(t *testing.T) {
|
func TestOutputFormat(t *testing.T) {
|
||||||
type testCase struct {
|
type testCase struct {
|
||||||
stdoutFormatFlag string
|
|
||||||
outputPath string
|
outputPath string
|
||||||
extension exportExtension
|
extension exportExtension
|
||||||
supportsDarkTheme bool
|
supportsDarkTheme bool
|
||||||
|
|
@ -42,15 +41,6 @@ func TestOutputFormat(t *testing.T) {
|
||||||
requiresAnimationInterval: false,
|
requiresAnimationInterval: false,
|
||||||
requiresPngRender: false,
|
requiresPngRender: false,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
stdoutFormatFlag: "png",
|
|
||||||
outputPath: "-",
|
|
||||||
extension: PNG,
|
|
||||||
supportsDarkTheme: false,
|
|
||||||
supportsAnimation: false,
|
|
||||||
requiresAnimationInterval: false,
|
|
||||||
requiresPngRender: true,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
outputPath: "/out.png",
|
outputPath: "/out.png",
|
||||||
extension: PNG,
|
extension: PNG,
|
||||||
|
|
@ -88,8 +78,7 @@ func TestOutputFormat(t *testing.T) {
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
tc := tc
|
tc := tc
|
||||||
t.Run(tc.outputPath, func(t *testing.T) {
|
t.Run(tc.outputPath, func(t *testing.T) {
|
||||||
extension, err := getOutputFormat(&tc.stdoutFormatFlag, tc.outputPath)
|
extension := getExportExtension(tc.outputPath)
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, tc.extension, extension)
|
assert.Equal(t, tc.extension, extension)
|
||||||
assert.Equal(t, tc.supportsAnimation, extension.supportsAnimation())
|
assert.Equal(t, tc.supportsAnimation, extension.supportsAnimation())
|
||||||
assert.Equal(t, tc.supportsDarkTheme, extension.supportsDarkTheme())
|
assert.Equal(t, tc.supportsDarkTheme, extension.supportsDarkTheme())
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,6 @@ Usage:
|
||||||
%[1]s [--watch=false] [--theme=0] file.d2 [file.svg | file.png]
|
%[1]s [--watch=false] [--theme=0] file.d2 [file.svg | file.png]
|
||||||
%[1]s layout [name]
|
%[1]s layout [name]
|
||||||
%[1]s fmt file.d2 ...
|
%[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
|
%[1]s compiles and renders file.d2 to file.svg | file.png
|
||||||
It defaults to file.svg if an output path is not provided.
|
It defaults to file.svg if an output path is not provided.
|
||||||
|
|
@ -40,8 +38,6 @@ Subcommands:
|
||||||
%[1]s layout [name] - Display long help for a particular layout engine, including its configuration options
|
%[1]s layout [name] - Display long help for a particular layout engine, including its configuration options
|
||||||
%[1]s themes - Lists available themes
|
%[1]s themes - Lists available themes
|
||||||
%[1]s fmt file.d2 ... - Format passed files
|
%[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.
|
See more docs and the source code at https://oss.terrastruct.com/d2.
|
||||||
Hosted icons at https://icons.terrastruct.com.
|
Hosted icons at https://icons.terrastruct.com.
|
||||||
|
|
@ -59,7 +55,7 @@ func layoutCmd(ctx context.Context, ms *xmain.State, ps []d2plugin.Plugin) error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func themesCmd(_ context.Context, ms *xmain.State) {
|
func themesCmd(ctx context.Context, ms *xmain.State) {
|
||||||
fmt.Fprintf(ms.Stdout, "Available themes:\n%s", d2themescatalog.CLIString())
|
fmt.Fprintf(ms.Stdout, "Available themes:\n%s", d2themescatalog.CLIString())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
100
d2cli/main.go
|
|
@ -50,7 +50,7 @@ import (
|
||||||
func Run(ctx context.Context, ms *xmain.State) (err error) {
|
func Run(ctx context.Context, ms *xmain.State) (err error) {
|
||||||
ctx = log.WithDefault(ctx)
|
ctx = log.WithDefault(ctx)
|
||||||
// These should be kept up-to-date with the d2 man page
|
// 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 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 is will open on a randomly available local port).")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -103,11 +103,6 @@ func Run(ctx context.Context, ms *xmain.State) (err error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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.")
|
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")
|
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 {
|
if err != nil {
|
||||||
|
|
@ -129,18 +124,6 @@ func Run(ctx context.Context, ms *xmain.State) (err error) {
|
||||||
return err
|
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)
|
plugins, err := d2plugin.ListPlugins(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -176,10 +159,6 @@ func Run(ctx context.Context, ms *xmain.State) (err error) {
|
||||||
return nil
|
return nil
|
||||||
case "fmt":
|
case "fmt":
|
||||||
return fmtCmd(ctx, ms, *checkFlag)
|
return fmtCmd(ctx, ms, *checkFlag)
|
||||||
case "play":
|
|
||||||
return playCmd(ctx, ms)
|
|
||||||
case "validate":
|
|
||||||
return validateCmd(ctx, ms)
|
|
||||||
case "version":
|
case "version":
|
||||||
if len(ms.Opts.Flags.Args()) > 1 {
|
if len(ms.Opts.Flags.Args()) > 1 {
|
||||||
return xmain.UsageErrorf("version subcommand accepts no arguments")
|
return xmain.UsageErrorf("version subcommand accepts no arguments")
|
||||||
|
|
@ -239,12 +218,7 @@ func Run(ctx context.Context, ms *xmain.State) (err error) {
|
||||||
if filepath.Ext(outputPath) == ".ppt" {
|
if filepath.Ext(outputPath) == ".ppt" {
|
||||||
return xmain.UsageErrorf("D2 does not support ppt exports, did you mean \"pptx\"?")
|
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 != "-" {
|
if outputPath != "-" {
|
||||||
outputPath = ms.AbsPath(outputPath)
|
outputPath = ms.AbsPath(outputPath)
|
||||||
if *animateIntervalFlag > 0 && !outputFormat.supportsAnimation() {
|
if *animateIntervalFlag > 0 && !outputFormat.supportsAnimation() {
|
||||||
|
|
@ -334,9 +308,6 @@ func Run(ctx context.Context, ms *xmain.State) (err error) {
|
||||||
ThemeID: themeFlag,
|
ThemeID: themeFlag,
|
||||||
DarkThemeID: darkThemeFlag,
|
DarkThemeID: darkThemeFlag,
|
||||||
Scale: scale,
|
Scale: scale,
|
||||||
NoXMLTag: noXMLTagFlag,
|
|
||||||
Salt: saltFlag,
|
|
||||||
OmitVersion: omitVersionFlag,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if *watchFlag {
|
if *watchFlag {
|
||||||
|
|
@ -359,7 +330,6 @@ func Run(ctx context.Context, ms *xmain.State) (err error) {
|
||||||
forceAppendix: *forceAppendixFlag,
|
forceAppendix: *forceAppendixFlag,
|
||||||
pw: pw,
|
pw: pw,
|
||||||
fontFamily: fontFamily,
|
fontFamily: fontFamily,
|
||||||
outputFormat: outputFormat,
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -390,7 +360,7 @@ func Run(ctx context.Context, ms *xmain.State) (err error) {
|
||||||
ctx, cancel := timelib.WithTimeout(ctx, time.Minute*2)
|
ctx, cancel := timelib.WithTimeout(ctx, time.Minute*2)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
_, written, err := compile(ctx, ms, plugins, nil, layoutFlag, renderOpts, fontFamily, *animateIntervalFlag, inputPath, outputPath, boardPath, noChildren, *bundleFlag, *forceAppendixFlag, pw.Page, outputFormat)
|
_, written, err := compile(ctx, ms, plugins, nil, layoutFlag, renderOpts, fontFamily, *animateIntervalFlag, inputPath, outputPath, boardPath, noChildren, *bundleFlag, *forceAppendixFlag, pw.Page)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if written {
|
if written {
|
||||||
return fmt.Errorf("failed to fully compile (partial render written) %s: %w", ms.HumanPath(inputPath), err)
|
return fmt.Errorf("failed to fully compile (partial render written) %s: %w", ms.HumanPath(inputPath), err)
|
||||||
|
|
@ -465,7 +435,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, ext exportExtension) (_ []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) (_ []byte, written bool, _ error) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
input, err := ms.ReadPath(inputPath)
|
input, err := ms.ReadPath(inputPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -530,7 +500,7 @@ func compile(ctx context.Context, ms *xmain.State, plugins []d2plugin.Plugin, fs
|
||||||
plugin, _ := d2plugin.FindPlugin(ctx, plugins, *opts.Layout)
|
plugin, _ := d2plugin.FindPlugin(ctx, plugins, *opts.Layout)
|
||||||
|
|
||||||
if animateInterval > 0 {
|
if animateInterval > 0 {
|
||||||
masterID, err := diagram.HashID(renderOpts.Salt)
|
masterID, err := diagram.HashID()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, err
|
return nil, false, err
|
||||||
}
|
}
|
||||||
|
|
@ -557,6 +527,7 @@ func compile(ctx context.Context, ms *xmain.State, plugins []d2plugin.Plugin, fs
|
||||||
return nil, false, err
|
return nil, false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ext := getExportExtension(outputPath)
|
||||||
switch ext {
|
switch ext {
|
||||||
case GIF:
|
case GIF:
|
||||||
svg, pngs, err := renderPNGsForGIF(ctx, ms, plugin, renderOpts, ruler, page, inputPath, diagram)
|
svg, pngs, err := renderPNGsForGIF(ctx, ms, plugin, renderOpts, ruler, page, inputPath, diagram)
|
||||||
|
|
@ -632,9 +603,9 @@ func compile(ctx context.Context, ms *xmain.State, plugins []d2plugin.Plugin, fs
|
||||||
var boards [][]byte
|
var boards [][]byte
|
||||||
var err error
|
var err error
|
||||||
if noChildren {
|
if noChildren {
|
||||||
boards, err = renderSingle(ctx, ms, compileDur, plugin, renderOpts, inputPath, outputPath, bundle, forceAppendix, page, ruler, diagram, ext)
|
boards, err = renderSingle(ctx, ms, compileDur, plugin, renderOpts, inputPath, outputPath, bundle, forceAppendix, page, ruler, diagram)
|
||||||
} else {
|
} else {
|
||||||
boards, err = render(ctx, ms, compileDur, plugin, renderOpts, inputPath, outputPath, bundle, forceAppendix, page, ruler, diagram, ext)
|
boards, err = render(ctx, ms, compileDur, plugin, renderOpts, inputPath, outputPath, bundle, forceAppendix, page, ruler, diagram)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, err
|
return nil, false, err
|
||||||
|
|
@ -773,7 +744,7 @@ func relink(currDiagramPath string, d *d2target.Diagram, linkToOutput map[string
|
||||||
return nil
|
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, ext exportExtension) ([][]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) ([][]byte, error) {
|
||||||
if diagram.Name != "" {
|
if diagram.Name != "" {
|
||||||
ext := filepath.Ext(outputPath)
|
ext := filepath.Ext(outputPath)
|
||||||
outputPath = strings.TrimSuffix(outputPath, ext)
|
outputPath = strings.TrimSuffix(outputPath, ext)
|
||||||
|
|
@ -819,21 +790,21 @@ func render(ctx context.Context, ms *xmain.State, compileDur time.Duration, plug
|
||||||
|
|
||||||
var boards [][]byte
|
var boards [][]byte
|
||||||
for _, dl := range diagram.Layers {
|
for _, dl := range diagram.Layers {
|
||||||
childrenBoards, err := render(ctx, ms, compileDur, plugin, opts, inputPath, layersOutputPath, bundle, forceAppendix, page, ruler, dl, ext)
|
childrenBoards, err := render(ctx, ms, compileDur, plugin, opts, inputPath, layersOutputPath, bundle, forceAppendix, page, ruler, dl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
boards = append(boards, childrenBoards...)
|
boards = append(boards, childrenBoards...)
|
||||||
}
|
}
|
||||||
for _, dl := range diagram.Scenarios {
|
for _, dl := range diagram.Scenarios {
|
||||||
childrenBoards, err := render(ctx, ms, compileDur, plugin, opts, inputPath, scenariosOutputPath, bundle, forceAppendix, page, ruler, dl, ext)
|
childrenBoards, err := render(ctx, ms, compileDur, plugin, opts, inputPath, scenariosOutputPath, bundle, forceAppendix, page, ruler, dl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
boards = append(boards, childrenBoards...)
|
boards = append(boards, childrenBoards...)
|
||||||
}
|
}
|
||||||
for _, dl := range diagram.Steps {
|
for _, dl := range diagram.Steps {
|
||||||
childrenBoards, err := render(ctx, ms, compileDur, plugin, opts, inputPath, stepsOutputPath, bundle, forceAppendix, page, ruler, dl, ext)
|
childrenBoards, err := render(ctx, ms, compileDur, plugin, opts, inputPath, stepsOutputPath, bundle, forceAppendix, page, ruler, dl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -842,7 +813,7 @@ func render(ctx context.Context, ms *xmain.State, compileDur time.Duration, plug
|
||||||
|
|
||||||
if !diagram.IsFolderOnly {
|
if !diagram.IsFolderOnly {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
out, err := _render(ctx, ms, plugin, opts, inputPath, boardOutputPath, bundle, forceAppendix, page, ruler, diagram, ext)
|
out, err := _render(ctx, ms, plugin, opts, inputPath, boardOutputPath, bundle, forceAppendix, page, ruler, diagram)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return boards, err
|
return boards, err
|
||||||
}
|
}
|
||||||
|
|
@ -856,9 +827,9 @@ func render(ctx context.Context, ms *xmain.State, compileDur time.Duration, plug
|
||||||
return boards, nil
|
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, outputFormat exportExtension) ([][]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) ([][]byte, error) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
out, err := _render(ctx, ms, plugin, opts, inputPath, outputPath, bundle, forceAppendix, page, ruler, diagram, outputFormat)
|
out, err := _render(ctx, ms, plugin, opts, inputPath, outputPath, bundle, forceAppendix, page, ruler, diagram)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return [][]byte{}, err
|
return [][]byte{}, err
|
||||||
}
|
}
|
||||||
|
|
@ -869,16 +840,15 @@ func renderSingle(ctx context.Context, ms *xmain.State, compileDur time.Duration
|
||||||
return [][]byte{out}, nil
|
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, outputFormat exportExtension) ([]byte, error) {
|
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 := outputFormat == PNG
|
toPNG := getExportExtension(outputPath) == PNG
|
||||||
|
|
||||||
var scale *float64
|
var scale *float64
|
||||||
if opts.Scale != nil {
|
if opts.Scale != nil {
|
||||||
scale = opts.Scale
|
scale = opts.Scale
|
||||||
} else if toPNG {
|
} else if toPNG {
|
||||||
scale = go2.Pointer(1.)
|
scale = go2.Pointer(1.)
|
||||||
}
|
}
|
||||||
renderOpts := &d2svg.RenderOpts{
|
svg, err := d2svg.Render(diagram, &d2svg.RenderOpts{
|
||||||
Pad: opts.Pad,
|
Pad: opts.Pad,
|
||||||
Sketch: opts.Sketch,
|
Sketch: opts.Sketch,
|
||||||
Center: opts.Center,
|
Center: opts.Center,
|
||||||
|
|
@ -887,12 +857,8 @@ func _render(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opts
|
||||||
DarkThemeID: opts.DarkThemeID,
|
DarkThemeID: opts.DarkThemeID,
|
||||||
ThemeOverrides: opts.ThemeOverrides,
|
ThemeOverrides: opts.ThemeOverrides,
|
||||||
DarkThemeOverrides: opts.DarkThemeOverrides,
|
DarkThemeOverrides: opts.DarkThemeOverrides,
|
||||||
NoXMLTag: opts.NoXMLTag,
|
|
||||||
Salt: opts.Salt,
|
|
||||||
Scale: scale,
|
Scale: scale,
|
||||||
OmitVersion: opts.OmitVersion,
|
})
|
||||||
}
|
|
||||||
svg, err := d2svg.Render(diagram, renderOpts)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -913,12 +879,12 @@ func _render(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opts
|
||||||
bundleErr = multierr.Combine(bundleErr, bundleErr2)
|
bundleErr = multierr.Combine(bundleErr, bundleErr2)
|
||||||
}
|
}
|
||||||
if forceAppendix && !toPNG {
|
if forceAppendix && !toPNG {
|
||||||
svg = appendix.Append(diagram, renderOpts, ruler, svg)
|
svg = appendix.Append(diagram, ruler, svg)
|
||||||
}
|
}
|
||||||
|
|
||||||
out := svg
|
out := svg
|
||||||
if toPNG {
|
if toPNG {
|
||||||
svg := appendix.Append(diagram, renderOpts, ruler, svg)
|
svg := appendix.Append(diagram, ruler, svg)
|
||||||
|
|
||||||
if !bundle {
|
if !bundle {
|
||||||
var bundleErr2 error
|
var bundleErr2 error
|
||||||
|
|
@ -976,7 +942,7 @@ func renderPDF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opt
|
||||||
scale = go2.Pointer(1.)
|
scale = go2.Pointer(1.)
|
||||||
}
|
}
|
||||||
|
|
||||||
renderOpts := &d2svg.RenderOpts{
|
svg, err = d2svg.Render(diagram, &d2svg.RenderOpts{
|
||||||
Pad: opts.Pad,
|
Pad: opts.Pad,
|
||||||
Sketch: opts.Sketch,
|
Sketch: opts.Sketch,
|
||||||
Center: opts.Center,
|
Center: opts.Center,
|
||||||
|
|
@ -985,9 +951,7 @@ func renderPDF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opt
|
||||||
DarkThemeID: opts.DarkThemeID,
|
DarkThemeID: opts.DarkThemeID,
|
||||||
ThemeOverrides: opts.ThemeOverrides,
|
ThemeOverrides: opts.ThemeOverrides,
|
||||||
DarkThemeOverrides: opts.DarkThemeOverrides,
|
DarkThemeOverrides: opts.DarkThemeOverrides,
|
||||||
OmitVersion: opts.OmitVersion,
|
})
|
||||||
}
|
|
||||||
svg, err = d2svg.Render(diagram, renderOpts)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -1005,7 +969,7 @@ func renderPDF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opt
|
||||||
if bundleErr != nil {
|
if bundleErr != nil {
|
||||||
return svg, bundleErr
|
return svg, bundleErr
|
||||||
}
|
}
|
||||||
svg = appendix.Append(diagram, renderOpts, ruler, svg)
|
svg = appendix.Append(diagram, ruler, svg)
|
||||||
|
|
||||||
pngImg, err := ConvertSVG(ms, page, svg)
|
pngImg, err := ConvertSVG(ms, page, svg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -1084,7 +1048,7 @@ func renderPPTX(ctx context.Context, ms *xmain.State, presentation *pptx.Present
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
renderOpts := &d2svg.RenderOpts{
|
svg, err = d2svg.Render(diagram, &d2svg.RenderOpts{
|
||||||
Pad: opts.Pad,
|
Pad: opts.Pad,
|
||||||
Sketch: opts.Sketch,
|
Sketch: opts.Sketch,
|
||||||
Center: opts.Center,
|
Center: opts.Center,
|
||||||
|
|
@ -1093,9 +1057,7 @@ func renderPPTX(ctx context.Context, ms *xmain.State, presentation *pptx.Present
|
||||||
DarkThemeID: opts.DarkThemeID,
|
DarkThemeID: opts.DarkThemeID,
|
||||||
ThemeOverrides: opts.ThemeOverrides,
|
ThemeOverrides: opts.ThemeOverrides,
|
||||||
DarkThemeOverrides: opts.DarkThemeOverrides,
|
DarkThemeOverrides: opts.DarkThemeOverrides,
|
||||||
OmitVersion: opts.OmitVersion,
|
})
|
||||||
}
|
|
||||||
svg, err = d2svg.Render(diagram, renderOpts)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -1114,7 +1076,7 @@ func renderPPTX(ctx context.Context, ms *xmain.State, presentation *pptx.Present
|
||||||
return nil, bundleErr
|
return nil, bundleErr
|
||||||
}
|
}
|
||||||
|
|
||||||
svg = appendix.Append(diagram, renderOpts, ruler, svg)
|
svg = appendix.Append(diagram, ruler, svg)
|
||||||
|
|
||||||
pngImg, err := ConvertSVG(ms, page, svg)
|
pngImg, err := ConvertSVG(ms, page, svg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -1332,7 +1294,7 @@ func renderPNGsForGIF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plug
|
||||||
} else {
|
} else {
|
||||||
scale = go2.Pointer(1.)
|
scale = go2.Pointer(1.)
|
||||||
}
|
}
|
||||||
renderOpts := &d2svg.RenderOpts{
|
svg, err = d2svg.Render(diagram, &d2svg.RenderOpts{
|
||||||
Pad: opts.Pad,
|
Pad: opts.Pad,
|
||||||
Sketch: opts.Sketch,
|
Sketch: opts.Sketch,
|
||||||
Center: opts.Center,
|
Center: opts.Center,
|
||||||
|
|
@ -1341,9 +1303,7 @@ func renderPNGsForGIF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plug
|
||||||
DarkThemeID: opts.DarkThemeID,
|
DarkThemeID: opts.DarkThemeID,
|
||||||
ThemeOverrides: opts.ThemeOverrides,
|
ThemeOverrides: opts.ThemeOverrides,
|
||||||
DarkThemeOverrides: opts.DarkThemeOverrides,
|
DarkThemeOverrides: opts.DarkThemeOverrides,
|
||||||
OmitVersion: opts.OmitVersion,
|
})
|
||||||
}
|
|
||||||
svg, err = d2svg.Render(diagram, renderOpts)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -1362,7 +1322,7 @@ func renderPNGsForGIF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plug
|
||||||
return nil, nil, bundleErr
|
return nil, nil, bundleErr
|
||||||
}
|
}
|
||||||
|
|
||||||
svg = appendix.Append(diagram, renderOpts, ruler, svg)
|
svg = appendix.Append(diagram, ruler, svg)
|
||||||
|
|
||||||
pngImg, err := ConvertSVG(ms, page, svg)
|
pngImg, err := ConvertSVG(ms, page, svg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -1,75 +0,0 @@
|
||||||
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");
|
const parsedXML = new DOMParser().parseFromString(msg.svg, "text/xml");
|
||||||
d2SVG.replaceChildren(parsedXML.documentElement);
|
d2SVG.replaceChildren(parsedXML.documentElement);
|
||||||
changeFavicon("/static/favicon.ico");
|
changeFavicon("/static/favicon.ico");
|
||||||
const svgEl = d2SVG.querySelector(".d2-svg");
|
const svgEl = d2SVG.querySelector("#d2-svg");
|
||||||
// just use inner SVG in watch mode
|
// just use inner SVG in watch mode
|
||||||
svgEl.parentElement.replaceWith(svgEl);
|
svgEl.parentElement.replaceWith(svgEl);
|
||||||
let width = parseInt(svgEl.getAttribute("width"), 10);
|
let width = parseInt(svgEl.getAttribute("width"), 10);
|
||||||
|
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
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"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/coder/websocket"
|
|
||||||
"github.com/coder/websocket/wsjson"
|
|
||||||
"github.com/fsnotify/fsnotify"
|
"github.com/fsnotify/fsnotify"
|
||||||
|
"nhooyr.io/websocket"
|
||||||
|
"nhooyr.io/websocket/wsjson"
|
||||||
|
|
||||||
"oss.terrastruct.com/util-go/xbrowser"
|
"oss.terrastruct.com/util-go/xbrowser"
|
||||||
|
|
||||||
|
|
@ -57,7 +57,6 @@ type watcherOpts struct {
|
||||||
forceAppendix bool
|
forceAppendix bool
|
||||||
pw png.Playwright
|
pw png.Playwright
|
||||||
fontFamily *d2fonts.FontFamily
|
fontFamily *d2fonts.FontFamily
|
||||||
outputFormat exportExtension
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type watcher struct {
|
type watcher struct {
|
||||||
|
|
@ -264,12 +263,6 @@ func (w *watcher) watchLoop(ctx context.Context) error {
|
||||||
return errors.New("fsnotify watcher closed")
|
return errors.New("fsnotify watcher closed")
|
||||||
}
|
}
|
||||||
w.ms.Log.Debug.Printf("received file system event %v", ev)
|
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)
|
mt, err := w.ensureAddWatch(ctx, ev.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -355,11 +348,6 @@ func (w *watcher) ensureAddWatch(ctx context.Context, path string) (time.Time, e
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *watcher) addWatch(ctx context.Context, path string) (time.Time, error) {
|
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)
|
err := w.fw.Add(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return time.Time{}, err
|
return time.Time{}, err
|
||||||
|
|
@ -442,7 +430,7 @@ func (w *watcher) compileLoop(ctx context.Context) error {
|
||||||
if w.boardPath != "" {
|
if w.boardPath != "" {
|
||||||
boardPath = strings.Split(w.boardPath, string(os.PathSeparator))
|
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, w.outputFormat)
|
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.boardpathMu.Unlock()
|
w.boardpathMu.Unlock()
|
||||||
errs := ""
|
errs := ""
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -682,41 +670,3 @@ func (tfs *trackedFS) Open(name string) (fs.File, error) {
|
||||||
}
|
}
|
||||||
return f, err
|
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, ""
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@ import (
|
||||||
"oss.terrastruct.com/d2/d2parser"
|
"oss.terrastruct.com/d2/d2parser"
|
||||||
"oss.terrastruct.com/d2/d2target"
|
"oss.terrastruct.com/d2/d2target"
|
||||||
"oss.terrastruct.com/d2/lib/color"
|
"oss.terrastruct.com/d2/lib/color"
|
||||||
"oss.terrastruct.com/d2/lib/geo"
|
|
||||||
"oss.terrastruct.com/d2/lib/textmeasure"
|
"oss.terrastruct.com/d2/lib/textmeasure"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -88,7 +87,6 @@ func (c *compiler) compileBoard(g *d2graph.Graph, ir *d2ir.Map) *d2graph.Graph {
|
||||||
ir = ir.Copy(nil).(*d2ir.Map)
|
ir = ir.Copy(nil).(*d2ir.Map)
|
||||||
// c.preprocessSeqDiagrams(ir)
|
// c.preprocessSeqDiagrams(ir)
|
||||||
c.compileMap(g.Root, ir)
|
c.compileMap(g.Root, ir)
|
||||||
c.setDefaultShapes(g)
|
|
||||||
if len(c.err.Errors) == 0 {
|
if len(c.err.Errors) == 0 {
|
||||||
c.validateKeys(g.Root, ir)
|
c.validateKeys(g.Root, ir)
|
||||||
}
|
}
|
||||||
|
|
@ -97,8 +95,6 @@ func (c *compiler) compileBoard(g *d2graph.Graph, ir *d2ir.Map) *d2graph.Graph {
|
||||||
c.validateEdges(g)
|
c.validateEdges(g)
|
||||||
c.validatePositionsCompatibility(g)
|
c.validatePositionsCompatibility(g)
|
||||||
|
|
||||||
c.compileLegend(g, ir)
|
|
||||||
|
|
||||||
c.compileBoardsField(g, ir, "layers")
|
c.compileBoardsField(g, ir, "layers")
|
||||||
c.compileBoardsField(g, ir, "scenarios")
|
c.compileBoardsField(g, ir, "scenarios")
|
||||||
c.compileBoardsField(g, ir, "steps")
|
c.compileBoardsField(g, ir, "steps")
|
||||||
|
|
@ -113,53 +109,6 @@ func (c *compiler) compileBoard(g *d2graph.Graph, ir *d2ir.Map) *d2graph.Graph {
|
||||||
return g
|
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) {
|
func (c *compiler) compileBoardsField(g *d2graph.Graph, ir *d2ir.Map, fieldName string) {
|
||||||
boards := ir.GetField(d2ast.FlatUnquotedString(fieldName))
|
boards := ir.GetField(d2ast.FlatUnquotedString(fieldName))
|
||||||
if boards.Map() == nil {
|
if boards.Map() == nil {
|
||||||
|
|
@ -208,49 +157,56 @@ func findFieldAST(ast *d2ast.Map, f *d2ir.Field) *d2ast.Map {
|
||||||
curr = d2ir.ParentField(curr)
|
curr = d2ir.ParentField(curr)
|
||||||
}
|
}
|
||||||
|
|
||||||
return _findFieldAST(ast, path)
|
currAST := ast
|
||||||
}
|
for len(path) > 0 {
|
||||||
|
|
||||||
func _findFieldAST(ast *d2ast.Map, path []string) *d2ast.Map {
|
|
||||||
if len(path) == 0 {
|
|
||||||
return ast
|
|
||||||
}
|
|
||||||
|
|
||||||
head := path[0]
|
head := path[0]
|
||||||
remainingPath := path[1:]
|
found := false
|
||||||
|
for _, n := range currAST.Nodes {
|
||||||
for i := range ast.Nodes {
|
if n.MapKey == nil {
|
||||||
if ast.Nodes[i].MapKey == nil || ast.Nodes[i].MapKey.Key == nil || len(ast.Nodes[i].MapKey.Key.Path) != 1 {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if n.MapKey.Key == nil {
|
||||||
head2 := ast.Nodes[i].MapKey.Key.Path[0].Unbox().ScalarString()
|
continue
|
||||||
|
}
|
||||||
|
if len(n.MapKey.Key.Path) != 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
head2 := n.MapKey.Key.Path[0].Unbox().ScalarString()
|
||||||
if head == head2 {
|
if head == head2 {
|
||||||
if ast.Nodes[i].MapKey.Value.Map == nil {
|
currAST = n.MapKey.Value.Map
|
||||||
ast.Nodes[i].MapKey.Value.Map = &d2ast.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"),
|
Range: d2ast.MakeRange(",1:0:0-1:0:0"),
|
||||||
}
|
}
|
||||||
if ast.Nodes[i].MapKey.Value.Import != nil {
|
if n.MapKey.Value.Import != nil {
|
||||||
imp := &d2ast.Import{
|
imp := &d2ast.Import{
|
||||||
Range: d2ast.MakeRange(",1:0:0-1:0:0"),
|
Range: d2ast.MakeRange(",1:0:0-1:0:0"),
|
||||||
Spread: true,
|
Spread: true,
|
||||||
Pre: ast.Nodes[i].MapKey.Value.Import.Pre,
|
Pre: n.MapKey.Value.Import.Pre,
|
||||||
Path: ast.Nodes[i].MapKey.Value.Import.Path,
|
Path: n.MapKey.Value.Import.Path,
|
||||||
}
|
}
|
||||||
ast.Nodes[i].MapKey.Value.Map.Nodes = append(ast.Nodes[i].MapKey.Value.Map.Nodes, d2ast.MapNodeBox{
|
n.MapKey.Value.Map.Nodes = append(n.MapKey.Value.Map.Nodes, d2ast.MapNodeBox{
|
||||||
Import: imp,
|
Import: imp,
|
||||||
})
|
})
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if result := _findFieldAST(ast.Nodes[i].MapKey.Value.Map, remainingPath); result != nil {
|
}
|
||||||
return result
|
currAST = n.MapKey.Value.Map
|
||||||
|
}
|
||||||
|
found = true
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
if !found {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
path = path[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
return currAST
|
||||||
|
}
|
||||||
|
|
||||||
type compiler struct {
|
type compiler struct {
|
||||||
err *d2parser.ParseError
|
err *d2parser.ParseError
|
||||||
|
|
@ -382,7 +338,7 @@ func (c *compiler) compileField(obj *d2graph.Object, f *d2ir.Field) {
|
||||||
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"`)
|
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
|
return
|
||||||
}
|
}
|
||||||
c.compileStyle(&obj.Attributes.Style, f.Map())
|
c.compileStyle(&obj.Attributes, f.Map())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -447,6 +403,8 @@ func (c *compiler) compileLabel(attrs *d2graph.Attributes, f d2ir.Node) {
|
||||||
attrs.Language = fullTag
|
attrs.Language = fullTag
|
||||||
}
|
}
|
||||||
switch attrs.Language {
|
switch attrs.Language {
|
||||||
|
case "latex":
|
||||||
|
attrs.Shape.Value = d2target.ShapeText
|
||||||
case "markdown":
|
case "markdown":
|
||||||
rendered, err := textmeasure.RenderMarkdown(scalar.ScalarString())
|
rendered, err := textmeasure.RenderMarkdown(scalar.ScalarString())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -463,6 +421,9 @@ func (c *compiler) compileLabel(attrs *d2graph.Attributes, f d2ir.Node) {
|
||||||
c.errorf(f.LastPrimaryKey(), "malformed Markdown: %s", err.Error())
|
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()
|
attrs.Label.Value = scalar.ScalarString()
|
||||||
default:
|
default:
|
||||||
|
|
@ -553,14 +514,13 @@ func (c *compiler) compileReserved(attrs *d2graph.Attributes, f *d2ir.Field) {
|
||||||
c.compileLabel(attrs, f)
|
c.compileLabel(attrs, f)
|
||||||
c.compilePosition(attrs, f)
|
c.compilePosition(attrs, f)
|
||||||
case "shape":
|
case "shape":
|
||||||
shapeVal := strings.ToLower(scalar.ScalarString())
|
in := d2target.IsShape(scalar.ScalarString())
|
||||||
in := d2target.IsShape(shapeVal)
|
_, isArrowhead := d2target.Arrowheads[scalar.ScalarString()]
|
||||||
_, isArrowhead := d2target.Arrowheads[shapeVal]
|
|
||||||
if !in && !isArrowhead {
|
if !in && !isArrowhead {
|
||||||
c.errorf(scalar, "unknown shape %q", scalar.ScalarString())
|
c.errorf(scalar, "unknown shape %q", scalar.ScalarString())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
attrs.Shape.Value = shapeVal
|
attrs.Shape.Value = scalar.ScalarString()
|
||||||
if strings.EqualFold(attrs.Shape.Value, d2target.ShapeCode) {
|
if strings.EqualFold(attrs.Shape.Value, d2target.ShapeCode) {
|
||||||
// Explicit code shape is plaintext.
|
// Explicit code shape is plaintext.
|
||||||
attrs.Language = d2target.ShapeText
|
attrs.Language = d2target.ShapeText
|
||||||
|
|
@ -574,17 +534,6 @@ func (c *compiler) compileReserved(attrs *d2graph.Attributes, f *d2ir.Field) {
|
||||||
}
|
}
|
||||||
attrs.Icon = iconURL
|
attrs.Icon = iconURL
|
||||||
c.compilePosition(attrs, f)
|
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":
|
case "near":
|
||||||
nearKey, err := d2parser.ParseKey(scalar.ScalarString())
|
nearKey, err := d2parser.ParseKey(scalar.ScalarString())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -647,12 +596,11 @@ func (c *compiler) compileReserved(attrs *d2graph.Attributes, f *d2ir.Field) {
|
||||||
attrs.Link.MapKey = f.LastPrimaryKey()
|
attrs.Link.MapKey = f.LastPrimaryKey()
|
||||||
case "direction":
|
case "direction":
|
||||||
dirs := []string{"up", "down", "right", "left"}
|
dirs := []string{"up", "down", "right", "left"}
|
||||||
val := strings.ToLower(scalar.ScalarString())
|
if !go2.Contains(dirs, scalar.ScalarString()) {
|
||||||
if !go2.Contains(dirs, val) {
|
|
||||||
c.errorf(scalar, `direction must be one of %v, got %q`, strings.Join(dirs, ", "), scalar.ScalarString())
|
c.errorf(scalar, `direction must be one of %v, got %q`, strings.Join(dirs, ", "), scalar.ScalarString())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
attrs.Direction.Value = val
|
attrs.Direction.Value = scalar.ScalarString()
|
||||||
attrs.Direction.MapKey = f.LastPrimaryKey()
|
attrs.Direction.MapKey = f.LastPrimaryKey()
|
||||||
case "constraint":
|
case "constraint":
|
||||||
if _, ok := scalar.(d2ast.String); !ok {
|
if _, ok := scalar.(d2ast.String); !ok {
|
||||||
|
|
@ -745,13 +693,13 @@ func (c *compiler) compileReserved(attrs *d2graph.Attributes, f *d2ir.Field) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *compiler) compileStyle(styles *d2graph.Style, m *d2ir.Map) {
|
func (c *compiler) compileStyle(attrs *d2graph.Attributes, m *d2ir.Map) {
|
||||||
for _, f := range m.Fields {
|
for _, f := range m.Fields {
|
||||||
c.compileStyleField(styles, f)
|
c.compileStyleField(attrs, f)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *compiler) compileStyleField(styles *d2graph.Style, f *d2ir.Field) {
|
func (c *compiler) compileStyleField(attrs *d2graph.Attributes, f *d2ir.Field) {
|
||||||
if _, ok := d2ast.StyleKeywords[strings.ToLower(f.Name.ScalarString())]; !(ok && f.Name.IsUnquoted()) {
|
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())
|
c.errorf(f.LastRef().AST(), `invalid style keyword: "%s"`, f.Name.ScalarString())
|
||||||
return
|
return
|
||||||
|
|
@ -759,57 +707,65 @@ func (c *compiler) compileStyleField(styles *d2graph.Style, f *d2ir.Field) {
|
||||||
if f.Primary() == nil {
|
if f.Primary() == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
compileStyleFieldInit(styles, f)
|
compileStyleFieldInit(attrs, f)
|
||||||
scalar := f.Primary().Value
|
scalar := f.Primary().Value
|
||||||
err := styles.Apply(f.Name.ScalarString(), scalar.ScalarString())
|
err := attrs.Style.Apply(f.Name.ScalarString(), scalar.ScalarString())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.errorf(scalar, err.Error())
|
c.errorf(scalar, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func compileStyleFieldInit(styles *d2graph.Style, f *d2ir.Field) {
|
func compileStyleFieldInit(attrs *d2graph.Attributes, f *d2ir.Field) {
|
||||||
switch f.Name.ScalarString() {
|
switch f.Name.ScalarString() {
|
||||||
case "opacity":
|
case "opacity":
|
||||||
styles.Opacity = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
attrs.Style.Opacity = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
||||||
case "stroke":
|
case "stroke":
|
||||||
styles.Stroke = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
attrs.Style.Stroke = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
||||||
case "fill":
|
case "fill":
|
||||||
styles.Fill = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
attrs.Style.Fill = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
||||||
case "fill-pattern":
|
case "fill-pattern":
|
||||||
styles.FillPattern = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
attrs.Style.FillPattern = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
||||||
case "stroke-width":
|
case "stroke-width":
|
||||||
styles.StrokeWidth = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
attrs.Style.StrokeWidth = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
||||||
case "stroke-dash":
|
case "stroke-dash":
|
||||||
styles.StrokeDash = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
attrs.Style.StrokeDash = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
||||||
case "border-radius":
|
case "border-radius":
|
||||||
styles.BorderRadius = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
attrs.Style.BorderRadius = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
||||||
case "shadow":
|
case "shadow":
|
||||||
styles.Shadow = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
attrs.Style.Shadow = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
||||||
case "3d":
|
case "3d":
|
||||||
styles.ThreeDee = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
attrs.Style.ThreeDee = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
||||||
case "multiple":
|
case "multiple":
|
||||||
styles.Multiple = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
attrs.Style.Multiple = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
||||||
case "font":
|
case "font":
|
||||||
styles.Font = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
attrs.Style.Font = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
||||||
case "font-size":
|
case "font-size":
|
||||||
styles.FontSize = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
attrs.Style.FontSize = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
||||||
case "font-color":
|
case "font-color":
|
||||||
styles.FontColor = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
attrs.Style.FontColor = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
||||||
case "animated":
|
case "animated":
|
||||||
styles.Animated = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
attrs.Style.Animated = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
||||||
case "bold":
|
case "bold":
|
||||||
styles.Bold = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
attrs.Style.Bold = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
||||||
case "italic":
|
case "italic":
|
||||||
styles.Italic = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
attrs.Style.Italic = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
||||||
case "underline":
|
case "underline":
|
||||||
styles.Underline = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
attrs.Style.Underline = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
||||||
case "filled":
|
case "filled":
|
||||||
styles.Filled = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
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()}
|
||||||
case "double-border":
|
case "double-border":
|
||||||
styles.DoubleBorder = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
attrs.Style.DoubleBorder = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
||||||
case "text-transform":
|
case "text-transform":
|
||||||
styles.TextTransform = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
attrs.Style.TextTransform = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -896,7 +852,7 @@ func (c *compiler) compileEdgeField(edge *d2graph.Edge, f *d2ir.Field) {
|
||||||
if f.Map() == nil {
|
if f.Map() == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.compileStyle(&edge.Attributes.Style, f.Map())
|
c.compileStyle(&edge.Attributes, f.Map())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -935,7 +891,7 @@ func (c *compiler) compileArrowheads(edge *d2graph.Edge, f *d2ir.Field) {
|
||||||
if f2.Map() == nil {
|
if f2.Map() == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
c.compileStyle(&attrs.Style, f2.Map())
|
c.compileStyle(attrs, f2.Map())
|
||||||
continue
|
continue
|
||||||
} else {
|
} else {
|
||||||
c.errorf(f2.LastRef().AST(), `source-arrowhead/target-arrowhead map keys must be reserved keywords`)
|
c.errorf(f2.LastRef().AST(), `source-arrowhead/target-arrowhead map keys must be reserved keywords`)
|
||||||
|
|
@ -1261,7 +1217,7 @@ func (c *compiler) validateBoardLinks(g *d2graph.Graph) {
|
||||||
}
|
}
|
||||||
|
|
||||||
u, err := url.Parse(html.UnescapeString(obj.Link.Value))
|
u, err := url.Parse(html.UnescapeString(obj.Link.Value))
|
||||||
isRemote := err == nil && (u.Scheme != "" || strings.HasPrefix(u.Path, "/"))
|
isRemote := err == nil && u.Scheme != ""
|
||||||
if isRemote {
|
if isRemote {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
@ -1497,12 +1453,6 @@ func compileConfig(ir *d2ir.Map) (*d2target.Config, error) {
|
||||||
config.LayoutEngine = go2.Pointer(f.Primary().Value.ScalarString())
|
config.LayoutEngine = go2.Pointer(f.Primary().Value.ScalarString())
|
||||||
}
|
}
|
||||||
|
|
||||||
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"))
|
f = configMap.GetField(d2ast.FlatUnquotedString("theme-overrides"))
|
||||||
if f != nil {
|
if f != nil {
|
||||||
overrides, err := compileThemeOverrides(f.Map())
|
overrides, err := compileThemeOverrides(f.Map())
|
||||||
|
|
@ -1608,21 +1558,3 @@ FOR:
|
||||||
}
|
}
|
||||||
return nil, nil
|
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -328,7 +328,7 @@ containers: {
|
||||||
Steps
|
Steps
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
expErr: `d2/testdata/d2compiler/TestCompile/image_children_Steps.d2:4:3: steps must be declared at a board root scope`,
|
expErr: `d2/testdata/d2compiler/TestCompile/image_children_Steps.d2:4:3: steps is only allowed at a board root`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "name-with-dot-underscore",
|
name: "name-with-dot-underscore",
|
||||||
|
|
@ -720,142 +720,6 @@ x: {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "legend",
|
|
||||||
|
|
||||||
text: `
|
|
||||||
vars: {
|
|
||||||
d2-legend: {
|
|
||||||
User: "A person who interacts with the system" {
|
|
||||||
shape: person
|
|
||||||
style: {
|
|
||||||
fill: "#f5f5f5"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Database: "Stores application data" {
|
|
||||||
shape: cylinder
|
|
||||||
style.fill: "#b5d3ff"
|
|
||||||
}
|
|
||||||
|
|
||||||
HiddenShape: "This should not appear in the legend" {
|
|
||||||
style.opacity: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
User -> Database: "Reads data" {
|
|
||||||
style.stroke: "blue"
|
|
||||||
}
|
|
||||||
|
|
||||||
Database -> User: "Returns results" {
|
|
||||||
style.stroke-dash: 5
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
user: User
|
|
||||||
db: Database
|
|
||||||
user -> db: Uses
|
|
||||||
`,
|
|
||||||
assertions: func(t *testing.T, g *d2graph.Graph) {
|
|
||||||
if g.Legend == nil {
|
|
||||||
t.Fatal("Expected Legend to be non-nil")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Verify the correct objects are in the legend
|
|
||||||
if len(g.Legend.Objects) != 2 {
|
|
||||||
t.Errorf("Expected 2 objects in legend, got %d", len(g.Legend.Objects))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for User object
|
|
||||||
hasUser := false
|
|
||||||
hasDatabase := false
|
|
||||||
for _, obj := range g.Legend.Objects {
|
|
||||||
if obj.ID == "User" {
|
|
||||||
hasUser = true
|
|
||||||
if obj.Shape.Value != "person" {
|
|
||||||
t.Errorf("User shape incorrect, expected 'person', got: %s", obj.Shape.Value)
|
|
||||||
}
|
|
||||||
} else if obj.ID == "Database" {
|
|
||||||
hasDatabase = true
|
|
||||||
if obj.Shape.Value != "cylinder" {
|
|
||||||
t.Errorf("Database shape incorrect, expected 'cylinder', got: %s", obj.Shape.Value)
|
|
||||||
}
|
|
||||||
} else if obj.ID == "HiddenShape" {
|
|
||||||
t.Errorf("HiddenShape should not be in legend due to opacity: 0")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !hasUser {
|
|
||||||
t.Errorf("User object missing from legend")
|
|
||||||
}
|
|
||||||
if !hasDatabase {
|
|
||||||
t.Errorf("Database object missing from legend")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Verify the correct edges are in the legend
|
|
||||||
if len(g.Legend.Edges) != 2 {
|
|
||||||
t.Errorf("Expected 2 edges in legend, got %d", len(g.Legend.Edges))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for expected edges
|
|
||||||
hasReadsEdge := false
|
|
||||||
hasReturnsEdge := false
|
|
||||||
for _, edge := range g.Legend.Edges {
|
|
||||||
if edge.Label.Value == "Reads data" {
|
|
||||||
hasReadsEdge = true
|
|
||||||
// Check edge properties
|
|
||||||
if edge.Style.Stroke == nil {
|
|
||||||
t.Errorf("Reads edge stroke is nil")
|
|
||||||
} else if edge.Style.Stroke.Value != "blue" {
|
|
||||||
t.Errorf("Reads edge stroke incorrect, expected 'blue', got: %s", edge.Style.Stroke.Value)
|
|
||||||
}
|
|
||||||
} else if edge.Label.Value == "Returns results" {
|
|
||||||
hasReturnsEdge = true
|
|
||||||
// Check edge properties
|
|
||||||
if edge.Style.StrokeDash == nil {
|
|
||||||
t.Errorf("Returns edge stroke-dash is nil")
|
|
||||||
} else if edge.Style.StrokeDash.Value != "5" {
|
|
||||||
t.Errorf("Returns edge stroke-dash incorrect, expected '5', got: %s", edge.Style.StrokeDash.Value)
|
|
||||||
}
|
|
||||||
} else if edge.Label.Value == "Hidden connection" {
|
|
||||||
t.Errorf("Hidden connection should not be in legend due to opacity: 0")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !hasReadsEdge {
|
|
||||||
t.Errorf("'Reads data' edge missing from legend")
|
|
||||||
}
|
|
||||||
if !hasReturnsEdge {
|
|
||||||
t.Errorf("'Returns results' edge missing from legend")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Verify the regular diagram content is still there
|
|
||||||
userObj, hasUserObj := g.Root.HasChild([]string{"user"})
|
|
||||||
if !hasUserObj {
|
|
||||||
t.Errorf("Main diagram missing 'user' object")
|
|
||||||
} else if userObj.Label.Value != "User" {
|
|
||||||
t.Errorf("User label incorrect, expected 'User', got: %s", userObj.Label.Value)
|
|
||||||
}
|
|
||||||
|
|
||||||
dbObj, hasDBObj := g.Root.HasChild([]string{"db"})
|
|
||||||
if !hasDBObj {
|
|
||||||
t.Errorf("Main diagram missing 'db' object")
|
|
||||||
} else if dbObj.Label.Value != "Database" {
|
|
||||||
t.Errorf("DB label incorrect, expected 'Database', got: %s", dbObj.Label.Value)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check the main edge
|
|
||||||
if len(g.Edges) == 0 {
|
|
||||||
t.Errorf("No edges found in main diagram")
|
|
||||||
} else {
|
|
||||||
mainEdge := g.Edges[0]
|
|
||||||
if mainEdge.Label.Value != "Uses" {
|
|
||||||
t.Errorf("Main edge label incorrect, expected 'Uses', got: %s", mainEdge.Label.Value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "underscore_edge_nested",
|
name: "underscore_edge_nested",
|
||||||
|
|
||||||
|
|
@ -1658,22 +1522,6 @@ x -> y: {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "url_relative_link",
|
|
||||||
|
|
||||||
text: `x: {
|
|
||||||
link: /google
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
assertions: func(t *testing.T, g *d2graph.Graph) {
|
|
||||||
if len(g.Objects) != 1 {
|
|
||||||
t.Fatal(g.Objects)
|
|
||||||
}
|
|
||||||
if g.Objects[0].Link.Value != "/google" {
|
|
||||||
t.Fatal(g.Objects[0].Link.Value)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "non_url_link",
|
name: "non_url_link",
|
||||||
|
|
||||||
|
|
@ -1714,204 +1562,6 @@ steps: {
|
||||||
assert.Equal(t, 1, len(g.Layers[0].Steps))
|
assert.Equal(t, 1, len(g.Layers[0].Steps))
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "composite-glob-filter",
|
|
||||||
|
|
||||||
text: `
|
|
||||||
*: {
|
|
||||||
&shape: [a; b]
|
|
||||||
}
|
|
||||||
k
|
|
||||||
`,
|
|
||||||
expErr: `d2/testdata/d2compiler/TestCompile/composite-glob-filter.d2:3:3: glob filters cannot be composites
|
|
||||||
d2/testdata/d2compiler/TestCompile/composite-glob-filter.d2:3:3: glob filters cannot be composites`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "imported-glob-leaf-filter",
|
|
||||||
|
|
||||||
text: `
|
|
||||||
***: {
|
|
||||||
&leaf: true
|
|
||||||
style: {
|
|
||||||
font-size: 30
|
|
||||||
}
|
|
||||||
}
|
|
||||||
a: {
|
|
||||||
...@x
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
files: map[string]string{
|
|
||||||
"x.d2": `
|
|
||||||
b
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
assertions: func(t *testing.T, g *d2graph.Graph) {
|
|
||||||
assert.Equal(t, 2, len(g.Objects))
|
|
||||||
assert.Equal(t, "b", g.Objects[0].Label.Value)
|
|
||||||
assert.Equal(t, "a", g.Objects[1].Label.Value)
|
|
||||||
assert.Equal(t, "30", g.Objects[0].Style.FontSize.Value)
|
|
||||||
assert.Equal(t, (*d2graph.Scalar)(nil), g.Objects[1].Style.FontSize)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "import-nested-var",
|
|
||||||
|
|
||||||
text: `...@models.environment
|
|
||||||
`,
|
|
||||||
files: map[string]string{
|
|
||||||
"models.d2": `
|
|
||||||
vars: {
|
|
||||||
c: {
|
|
||||||
k
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
environment: {
|
|
||||||
...${c}
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
assertions: func(t *testing.T, g *d2graph.Graph) {
|
|
||||||
assert.Equal(t, 1, len(g.Objects))
|
|
||||||
assert.Equal(t, "k", g.Objects[0].AbsID())
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "import-connections",
|
|
||||||
|
|
||||||
text: `b.c -> b.d
|
|
||||||
|
|
||||||
b: @imp
|
|
||||||
`,
|
|
||||||
files: map[string]string{
|
|
||||||
"imp.d2": `
|
|
||||||
c
|
|
||||||
d
|
|
||||||
d -> c
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
assertions: func(t *testing.T, g *d2graph.Graph) {
|
|
||||||
assert.Equal(t, 2, len(g.Edges))
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "import-style-1",
|
|
||||||
|
|
||||||
text: `b.c.style.fill: red
|
|
||||||
|
|
||||||
b: @imp
|
|
||||||
`,
|
|
||||||
files: map[string]string{
|
|
||||||
"imp.d2": `c`,
|
|
||||||
},
|
|
||||||
assertions: func(t *testing.T, g *d2graph.Graph) {
|
|
||||||
assert.Equal(t, 2, len(g.Objects))
|
|
||||||
assert.Equal(t, "c", g.Objects[1].Label.Value)
|
|
||||||
assert.Equal(t, "red", g.Objects[1].Style.Fill.Value)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "import-style-2",
|
|
||||||
|
|
||||||
text: `b.k.c.style.fill: red
|
|
||||||
|
|
||||||
b: @imp
|
|
||||||
`,
|
|
||||||
files: map[string]string{
|
|
||||||
"imp.d2": `
|
|
||||||
k: {
|
|
||||||
c
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
assertions: func(t *testing.T, g *d2graph.Graph) {
|
|
||||||
assert.Equal(t, "c", g.Objects[2].Label.Value)
|
|
||||||
assert.Equal(t, "red", g.Objects[2].Style.Fill.Value)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "import-scenario",
|
|
||||||
|
|
||||||
text: `a
|
|
||||||
|
|
||||||
...@test
|
|
||||||
`,
|
|
||||||
files: map[string]string{
|
|
||||||
"test.d2": `
|
|
||||||
x
|
|
||||||
|
|
||||||
scenarios: {
|
|
||||||
production: {
|
|
||||||
x.tooltip: foo
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
assertions: func(t *testing.T, g *d2graph.Graph) {
|
|
||||||
assert.Equal(t, 2, len(g.Scenarios[0].Objects))
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "import-steps",
|
|
||||||
|
|
||||||
text: `a
|
|
||||||
|
|
||||||
...@test
|
|
||||||
`,
|
|
||||||
files: map[string]string{
|
|
||||||
"test.d2": `
|
|
||||||
x
|
|
||||||
|
|
||||||
steps: {
|
|
||||||
1: {
|
|
||||||
x.tooltip: foo
|
|
||||||
}
|
|
||||||
2: {
|
|
||||||
x.tooltip: do
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
assertions: func(t *testing.T, g *d2graph.Graph) {
|
|
||||||
assert.Equal(t, 2, len(g.Steps[0].Objects))
|
|
||||||
assert.Equal(t, 2, len(g.Steps[1].Objects))
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "import-classes-boards",
|
|
||||||
|
|
||||||
text: `classes: {
|
|
||||||
a: {
|
|
||||||
label: hi
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
layers: {
|
|
||||||
asdf: {
|
|
||||||
qwer: {
|
|
||||||
layers: {
|
|
||||||
ok: {
|
|
||||||
bok
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
wert: {
|
|
||||||
classes: @classes
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
`,
|
|
||||||
files: map[string]string{
|
|
||||||
"classes.d2": `
|
|
||||||
c: {
|
|
||||||
label: bye
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
expErr: `d2/testdata/d2compiler/TestCompile/import-classes-boards.d2:10:7: layers must be declared at a board root scope`,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "import_url_link",
|
name: "import_url_link",
|
||||||
|
|
||||||
|
|
@ -3339,62 +2989,6 @@ x*: {
|
||||||
tassert.Equal(t, "x2.ok", g.Objects[3].AbsID())
|
tassert.Equal(t, "x2.ok", g.Objects[3].AbsID())
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "glob-spread-vars/1",
|
|
||||||
text: `vars: {
|
|
||||||
b: {
|
|
||||||
1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
a: {
|
|
||||||
...${b}
|
|
||||||
*.style.fill: red
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
assertions: func(t *testing.T, g *d2graph.Graph) {
|
|
||||||
assert.Equal(t, "1", g.Objects[1].Label.Value)
|
|
||||||
assert.Equal(t, "red", g.Objects[1].Style.Fill.Value)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "glob-spread-vars/2",
|
|
||||||
text: `vars: {
|
|
||||||
b: {
|
|
||||||
1
|
|
||||||
2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
a: {
|
|
||||||
...${b}
|
|
||||||
** -> _.ok
|
|
||||||
}
|
|
||||||
|
|
||||||
ok
|
|
||||||
`,
|
|
||||||
assertions: func(t *testing.T, g *d2graph.Graph) {
|
|
||||||
assert.Equal(t, 2, len(g.Edges))
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "import-var-chain",
|
|
||||||
|
|
||||||
text: `...@dev
|
|
||||||
`,
|
|
||||||
files: map[string]string{
|
|
||||||
"dev.d2": `
|
|
||||||
vars: {
|
|
||||||
a: {
|
|
||||||
b
|
|
||||||
}
|
|
||||||
c: {
|
|
||||||
...${a}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "var_in_markdown",
|
name: "var_in_markdown",
|
||||||
text: `vars: {
|
text: `vars: {
|
||||||
|
|
@ -3422,22 +3016,6 @@ x: |md
|
||||||
tassert.True(t, strings.Contains(g.Objects[0].Attributes.Label.Value, "bye ${v}"))
|
tassert.True(t, strings.Contains(g.Objects[0].Attributes.Label.Value, "bye ${v}"))
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "var_nested_in_markdown",
|
|
||||||
text: `vars: {
|
|
||||||
v: {
|
|
||||||
g: ok
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
x: |md
|
|
||||||
m${v.g}y
|
|
||||||
|
|
|
||||||
`,
|
|
||||||
assertions: func(t *testing.T, g *d2graph.Graph) {
|
|
||||||
tassert.True(t, strings.Contains(g.Objects[0].Attributes.Label.Value, "moky"))
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "var_in_vars",
|
name: "var_in_vars",
|
||||||
text: `vars: {
|
text: `vars: {
|
||||||
|
|
@ -3956,14 +3534,6 @@ svc_1.t2 -> b: do with B
|
||||||
tassert.Equal(t, "d2/testdata/d2compiler/TestCompile/meow.d2", g.Layers[0].Layers[0].AST.Range.Path)
|
tassert.Equal(t, "d2/testdata/d2compiler/TestCompile/meow.d2", g.Layers[0].Layers[0].AST.Range.Path)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "invalid_gradient_color_stop",
|
|
||||||
text: `
|
|
||||||
x
|
|
||||||
x.style.fill: "linear-gradient(#ggg, #000)"
|
|
||||||
`,
|
|
||||||
expErr: `d2/testdata/d2compiler/TestCompile/invalid_gradient_color_stop.d2:3:19: expected "fill" to be a valid named color ("orange"), a hex code ("#f0ff3a"), or a gradient ("linear-gradient(red, blue)")`,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
|
|
@ -4315,7 +3885,7 @@ a: null
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "basic-edge",
|
name: "edge",
|
||||||
run: func(t *testing.T) {
|
run: func(t *testing.T) {
|
||||||
g, _ := assertCompile(t, `
|
g, _ := assertCompile(t, `
|
||||||
a -> b
|
a -> b
|
||||||
|
|
@ -4325,20 +3895,6 @@ a -> b
|
||||||
assert.Equal(t, 0, len(g.Edges))
|
assert.Equal(t, 0, len(g.Edges))
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "nested-edge",
|
|
||||||
run: func(t *testing.T) {
|
|
||||||
g, _ := assertCompile(t, `
|
|
||||||
a.b.c -> a.d.e
|
|
||||||
a.b.c -> a.d.e
|
|
||||||
|
|
||||||
a.(b.c -> d.e)[0]: null
|
|
||||||
(a.b.c -> a.d.e)[1]: null
|
|
||||||
`, "")
|
|
||||||
assert.Equal(t, 5, len(g.Objects))
|
|
||||||
assert.Equal(t, 0, len(g.Edges))
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "attribute",
|
name: "attribute",
|
||||||
run: func(t *testing.T) {
|
run: func(t *testing.T) {
|
||||||
|
|
@ -4839,24 +4395,6 @@ a: {
|
||||||
assert.Equal(t, 2, len(g.Objects[0].SQLTable.Columns[0].Constraint))
|
assert.Equal(t, 2, len(g.Objects[0].SQLTable.Columns[0].Constraint))
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "comment-array",
|
|
||||||
run: func(t *testing.T) {
|
|
||||||
assertCompile(t, `
|
|
||||||
vars: {
|
|
||||||
list: [
|
|
||||||
"a";
|
|
||||||
"b";
|
|
||||||
"c";
|
|
||||||
"d"
|
|
||||||
# e
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
a
|
|
||||||
`, "")
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "spread-array",
|
name: "spread-array",
|
||||||
run: func(t *testing.T) {
|
run: func(t *testing.T) {
|
||||||
|
|
@ -5659,155 +5197,6 @@ y.link: https://google.com
|
||||||
assert.Equal(t, "true", g.Objects[1].Attributes.Style.Underline.Value)
|
assert.Equal(t, "true", g.Objects[1].Attributes.Style.Underline.Value)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "leaf-filter-1",
|
|
||||||
run: func(t *testing.T) {
|
|
||||||
g, _ := assertCompile(t, `
|
|
||||||
**: {
|
|
||||||
&leaf: false
|
|
||||||
style.fill: red
|
|
||||||
}
|
|
||||||
**: {
|
|
||||||
&leaf: true
|
|
||||||
style.stroke: yellow
|
|
||||||
}
|
|
||||||
a.b.c
|
|
||||||
`, ``)
|
|
||||||
assert.Equal(t, "a", g.Objects[0].ID)
|
|
||||||
assert.Equal(t, "red", g.Objects[0].Attributes.Style.Fill.Value)
|
|
||||||
assert.Equal(t, (*d2graph.Scalar)(nil), g.Objects[0].Attributes.Style.Stroke)
|
|
||||||
assert.Equal(t, "b", g.Objects[1].ID)
|
|
||||||
assert.Equal(t, "red", g.Objects[1].Attributes.Style.Fill.Value)
|
|
||||||
assert.Equal(t, (*d2graph.Scalar)(nil), g.Objects[1].Attributes.Style.Stroke)
|
|
||||||
assert.Equal(t, "c", g.Objects[2].ID)
|
|
||||||
assert.Equal(t, (*d2graph.Scalar)(nil), g.Objects[2].Attributes.Style.Fill)
|
|
||||||
assert.Equal(t, "yellow", g.Objects[2].Attributes.Style.Stroke.Value)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "leaf-filter-2",
|
|
||||||
run: func(t *testing.T) {
|
|
||||||
g, _ := assertCompile(t, `
|
|
||||||
**: {
|
|
||||||
&leaf: true
|
|
||||||
style.stroke: yellow
|
|
||||||
}
|
|
||||||
a: {
|
|
||||||
b -> c
|
|
||||||
}
|
|
||||||
d: {
|
|
||||||
e
|
|
||||||
}
|
|
||||||
`, ``)
|
|
||||||
assert.Equal(t, "a", g.Objects[0].ID)
|
|
||||||
assert.Equal(t, (*d2graph.Scalar)(nil), g.Objects[0].Attributes.Style.Stroke)
|
|
||||||
assert.Equal(t, "b", g.Objects[1].ID)
|
|
||||||
assert.Equal(t, "yellow", g.Objects[1].Attributes.Style.Stroke.Value)
|
|
||||||
assert.Equal(t, "c", g.Objects[2].ID)
|
|
||||||
assert.Equal(t, "yellow", g.Objects[2].Attributes.Style.Stroke.Value)
|
|
||||||
assert.Equal(t, "d", g.Objects[3].ID)
|
|
||||||
assert.Equal(t, (*d2graph.Scalar)(nil), g.Objects[3].Attributes.Style.Stroke)
|
|
||||||
assert.Equal(t, "e", g.Objects[4].ID)
|
|
||||||
assert.Equal(t, "yellow", g.Objects[4].Attributes.Style.Stroke.Value)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "level-filter",
|
|
||||||
run: func(t *testing.T) {
|
|
||||||
g, _ := assertCompile(t, `
|
|
||||||
**: {
|
|
||||||
&level: 0
|
|
||||||
style.fill: red
|
|
||||||
}
|
|
||||||
**: {
|
|
||||||
&level: 1
|
|
||||||
style.stroke: yellow
|
|
||||||
}
|
|
||||||
(** -> **)[*]: {
|
|
||||||
&src.level: 0
|
|
||||||
&dst.level: 0
|
|
||||||
style.stroke: blue
|
|
||||||
}
|
|
||||||
a.b.c
|
|
||||||
|
|
||||||
x -> y
|
|
||||||
a: {
|
|
||||||
1 -> 2
|
|
||||||
}
|
|
||||||
a.1 -> x
|
|
||||||
`, ``)
|
|
||||||
assert.Equal(t, "a", g.Objects[0].ID)
|
|
||||||
assert.Equal(t, "red", g.Objects[0].Attributes.Style.Fill.Value)
|
|
||||||
assert.Equal(t, (*d2graph.Scalar)(nil), g.Objects[0].Attributes.Style.Stroke)
|
|
||||||
|
|
||||||
assert.Equal(t, "b", g.Objects[1].ID)
|
|
||||||
assert.Equal(t, "yellow", g.Objects[1].Attributes.Style.Stroke.Value)
|
|
||||||
assert.Equal(t, (*d2graph.Scalar)(nil), g.Objects[1].Attributes.Style.Fill)
|
|
||||||
|
|
||||||
assert.Equal(t, "c", g.Objects[2].ID)
|
|
||||||
assert.Equal(t, (*d2graph.Scalar)(nil), g.Objects[2].Attributes.Style.Fill)
|
|
||||||
assert.Equal(t, (*d2graph.Scalar)(nil), g.Objects[2].Attributes.Style.Stroke)
|
|
||||||
|
|
||||||
assert.Equal(t, "(x -> y)[0]", g.Edges[0].AbsID())
|
|
||||||
assert.Equal(t, "blue", g.Edges[0].Attributes.Style.Stroke.Value)
|
|
||||||
|
|
||||||
assert.Equal(t, "a.(1 -> 2)[0]", g.Edges[1].AbsID())
|
|
||||||
assert.Equal(t, (*d2graph.Scalar)(nil), g.Edges[1].Attributes.Style.Stroke)
|
|
||||||
|
|
||||||
assert.Equal(t, "(a.1 -> x)[0]", g.Edges[2].AbsID())
|
|
||||||
assert.Equal(t, (*d2graph.Scalar)(nil), g.Edges[2].Attributes.Style.Stroke)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "connected-filter",
|
|
||||||
run: func(t *testing.T) {
|
|
||||||
g, _ := assertCompile(t, `
|
|
||||||
*: {
|
|
||||||
&connected: true
|
|
||||||
style.fill: red
|
|
||||||
}
|
|
||||||
a -> b
|
|
||||||
c
|
|
||||||
`, ``)
|
|
||||||
assert.Equal(t, "a", g.Objects[0].ID)
|
|
||||||
assert.Equal(t, "red", g.Objects[0].Attributes.Style.Fill.Value)
|
|
||||||
assert.Equal(t, "b", g.Objects[1].ID)
|
|
||||||
assert.Equal(t, "red", g.Objects[1].Attributes.Style.Fill.Value)
|
|
||||||
assert.Equal(t, "c", g.Objects[2].ID)
|
|
||||||
assert.Equal(t, (*d2graph.Scalar)(nil), g.Objects[2].Attributes.Style.Fill)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "and-filter",
|
|
||||||
run: func(t *testing.T) {
|
|
||||||
g, _ := assertCompile(t, `
|
|
||||||
*: {
|
|
||||||
&shape: person
|
|
||||||
&connected: true
|
|
||||||
style.fill: red
|
|
||||||
}
|
|
||||||
(** -> **)[*]: {
|
|
||||||
&src: a
|
|
||||||
&dst: c
|
|
||||||
style.stroke: yellow
|
|
||||||
}
|
|
||||||
a -> b
|
|
||||||
a.shape: person
|
|
||||||
a -> c
|
|
||||||
`, ``)
|
|
||||||
assert.Equal(t, "a", g.Objects[0].ID)
|
|
||||||
assert.Equal(t, "red", g.Objects[0].Attributes.Style.Fill.Value)
|
|
||||||
assert.Equal(t, "b", g.Objects[1].ID)
|
|
||||||
assert.Equal(t, (*d2graph.Scalar)(nil), g.Objects[1].Attributes.Style.Fill)
|
|
||||||
assert.Equal(t, "c", g.Objects[2].ID)
|
|
||||||
assert.Equal(t, (*d2graph.Scalar)(nil), g.Objects[2].Attributes.Style.Fill)
|
|
||||||
|
|
||||||
assert.Equal(t, "(a -> b)[0]", g.Edges[0].AbsID())
|
|
||||||
assert.Equal(t, (*d2graph.Scalar)(nil), g.Edges[0].Attributes.Style.Stroke)
|
|
||||||
assert.Equal(t, "(a -> c)[0]", g.Edges[1].AbsID())
|
|
||||||
assert.Equal(t, "yellow", g.Edges[1].Attributes.Style.Stroke.Value)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "glob-filter",
|
name: "glob-filter",
|
||||||
run: func(t *testing.T) {
|
run: func(t *testing.T) {
|
||||||
|
|
@ -5941,306 +5330,6 @@ b -> c
|
||||||
assert.Equal(t, "red", g.Edges[0].Style.Stroke.Value)
|
assert.Equal(t, "red", g.Edges[0].Style.Stroke.Value)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "merge-glob-values",
|
|
||||||
run: func(t *testing.T) {
|
|
||||||
assertCompile(t, `
|
|
||||||
"a"
|
|
||||||
*.style.stroke-width: 2
|
|
||||||
*.style.font-size: 14
|
|
||||||
a.width: 339
|
|
||||||
`, ``)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "mixed-edge-quoting",
|
|
||||||
run: func(t *testing.T) {
|
|
||||||
g, _ := assertCompile(t, `
|
|
||||||
"a"."b"."c"."z1" -> "a"."b"."c"."z2"
|
|
||||||
`, ``)
|
|
||||||
assert.Equal(t, 5, len(g.Objects))
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "suspension-lazy",
|
|
||||||
run: func(t *testing.T) {
|
|
||||||
g, _ := assertCompile(t, `
|
|
||||||
a -> b
|
|
||||||
c
|
|
||||||
**: suspend
|
|
||||||
(** -> **)[*]: suspend
|
|
||||||
d
|
|
||||||
`, ``)
|
|
||||||
assert.Equal(t, 1, len(g.Objects))
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "suspension-quotes",
|
|
||||||
run: func(t *testing.T) {
|
|
||||||
g, _ := assertCompile(t, `
|
|
||||||
a -> b
|
|
||||||
c
|
|
||||||
**: suspend
|
|
||||||
(** -> **)[*]: suspend
|
|
||||||
d: "suspend"
|
|
||||||
d -> d: "suspend"
|
|
||||||
`, ``)
|
|
||||||
assert.Equal(t, 1, len(g.Objects))
|
|
||||||
assert.Equal(t, 1, len(g.Edges))
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "unsuspend-edge-label",
|
|
||||||
run: func(t *testing.T) {
|
|
||||||
g, _ := assertCompile(t, `
|
|
||||||
a -> b: hello
|
|
||||||
c
|
|
||||||
**: suspend
|
|
||||||
(** -> **)[*]: suspend
|
|
||||||
|
|
||||||
(* -> *)[*]: unsuspend
|
|
||||||
`, ``)
|
|
||||||
assert.Equal(t, 2, len(g.Objects))
|
|
||||||
assert.Equal(t, 1, len(g.Edges))
|
|
||||||
assert.Equal(t, "hello", g.Edges[0].Label.Value)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "glob-edge-filter",
|
|
||||||
run: func(t *testing.T) {
|
|
||||||
g, _ := assertCompile(t, `
|
|
||||||
x -> y: {class: foo}
|
|
||||||
a -> b
|
|
||||||
|
|
||||||
|
|
||||||
(** -> **)[*]: {
|
|
||||||
&class: foo
|
|
||||||
source-arrowhead: 1
|
|
||||||
target-arrowhead: * {
|
|
||||||
shape: diamond
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`, ``)
|
|
||||||
assert.Equal(t, 2, len(g.Edges))
|
|
||||||
assert.Equal(t, "(x -> y)[0]", g.Edges[0].AbsID())
|
|
||||||
assert.Equal(t, "(a -> b)[0]", g.Edges[1].AbsID())
|
|
||||||
assert.Equal(t, "1", g.Edges[0].SrcArrowhead.Label.Value)
|
|
||||||
assert.Equal(t, (*d2graph.Attributes)(nil), g.Edges[1].SrcArrowhead)
|
|
||||||
assert.Equal(t, "diamond", g.Edges[0].DstArrowhead.Shape.Value)
|
|
||||||
assert.Equal(t, "*", g.Edges[0].DstArrowhead.Label.Value)
|
|
||||||
assert.Equal(t, (*d2graph.Attributes)(nil), g.Edges[1].DstArrowhead)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "unsuspend-edge-filter",
|
|
||||||
run: func(t *testing.T) {
|
|
||||||
g, _ := assertCompile(t, `
|
|
||||||
a -> b
|
|
||||||
**: suspend
|
|
||||||
(** -> **)[*]: suspend
|
|
||||||
(* -> *)[*]: unsuspend {
|
|
||||||
&dst: a
|
|
||||||
}
|
|
||||||
`, ``)
|
|
||||||
assert.Equal(t, 0, len(g.Objects))
|
|
||||||
assert.Equal(t, 0, len(g.Edges))
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "unsuspend-edge-child",
|
|
||||||
run: func(t *testing.T) {
|
|
||||||
g, _ := assertCompile(t, `
|
|
||||||
a: {
|
|
||||||
b -> c
|
|
||||||
}
|
|
||||||
|
|
||||||
**: suspend
|
|
||||||
(** -> **)[*]: suspend
|
|
||||||
(** -> **)[*]: unsuspend {
|
|
||||||
&dst: a.c
|
|
||||||
}
|
|
||||||
`, ``)
|
|
||||||
assert.Equal(t, 3, len(g.Objects))
|
|
||||||
assert.Equal(t, 1, len(g.Edges))
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "unsuspend-cross-container-edge-label",
|
|
||||||
run: func(t *testing.T) {
|
|
||||||
g, _ := assertCompile(t, `
|
|
||||||
a: {
|
|
||||||
b
|
|
||||||
}
|
|
||||||
c: {
|
|
||||||
d
|
|
||||||
}
|
|
||||||
a.b -> c.d: likes
|
|
||||||
**: suspend
|
|
||||||
(** -> **)[*]: suspend
|
|
||||||
(** -> **)[*]: unsuspend {
|
|
||||||
&label: likes
|
|
||||||
}
|
|
||||||
`, ``)
|
|
||||||
assert.Equal(t, 4, len(g.Objects))
|
|
||||||
assert.Equal(t, 1, len(g.Edges))
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "unsuspend-shape-label",
|
|
||||||
run: func(t *testing.T) {
|
|
||||||
g, _ := assertCompile(t, `
|
|
||||||
a: hello
|
|
||||||
*: suspend
|
|
||||||
*: unsuspend
|
|
||||||
`, ``)
|
|
||||||
assert.Equal(t, 1, len(g.Objects))
|
|
||||||
assert.Equal(t, "hello", g.Objects[0].Label.Value)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "suspend-shape",
|
|
||||||
run: func(t *testing.T) {
|
|
||||||
g, _ := assertCompile(t, `
|
|
||||||
a: hello
|
|
||||||
*: suspend
|
|
||||||
`, ``)
|
|
||||||
assert.Equal(t, 0, len(g.Objects))
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "edge-glob-ampersand-filter/1",
|
|
||||||
run: func(t *testing.T) {
|
|
||||||
g, _ := assertCompile(t, `
|
|
||||||
(* -> *)[*]: {
|
|
||||||
&src: a
|
|
||||||
style.stroke-dash: 3
|
|
||||||
}
|
|
||||||
(* -> *)[*]: {
|
|
||||||
&dst: c
|
|
||||||
style.stroke: blue
|
|
||||||
}
|
|
||||||
(* -> *)[*]: {
|
|
||||||
&src: b
|
|
||||||
&dst: c
|
|
||||||
style.fill: red
|
|
||||||
}
|
|
||||||
a -> b
|
|
||||||
b -> c
|
|
||||||
a -> c
|
|
||||||
`, ``)
|
|
||||||
tassert.Equal(t, 3, len(g.Edges))
|
|
||||||
|
|
||||||
tassert.Equal(t, "a", g.Edges[0].Src.ID)
|
|
||||||
tassert.Equal(t, "b", g.Edges[0].Dst.ID)
|
|
||||||
tassert.Equal(t, "3", g.Edges[0].Style.StrokeDash.Value)
|
|
||||||
tassert.Equal(t, (*d2graph.Scalar)(nil), g.Edges[0].Style.Stroke)
|
|
||||||
tassert.Equal(t, (*d2graph.Scalar)(nil), g.Edges[0].Style.Fill)
|
|
||||||
|
|
||||||
tassert.Equal(t, "b", g.Edges[1].Src.ID)
|
|
||||||
tassert.Equal(t, "c", g.Edges[1].Dst.ID)
|
|
||||||
tassert.Equal(t, "blue", g.Edges[1].Style.Stroke.Value)
|
|
||||||
tassert.Equal(t, (*d2graph.Scalar)(nil), g.Edges[1].Style.StrokeDash)
|
|
||||||
tassert.Equal(t, "red", g.Edges[1].Style.Fill.Value)
|
|
||||||
|
|
||||||
tassert.Equal(t, "a", g.Edges[2].Src.ID)
|
|
||||||
tassert.Equal(t, "c", g.Edges[2].Dst.ID)
|
|
||||||
tassert.Equal(t, "3", g.Edges[2].Style.StrokeDash.Value)
|
|
||||||
tassert.Equal(t, "blue", g.Edges[2].Style.Stroke.Value)
|
|
||||||
tassert.Equal(t, (*d2graph.Scalar)(nil), g.Edges[2].Style.Fill)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "edge-glob-ampersand-filter/2",
|
|
||||||
run: func(t *testing.T) {
|
|
||||||
g, _ := assertCompile(t, `
|
|
||||||
a: {
|
|
||||||
shape: circle
|
|
||||||
style: {
|
|
||||||
fill: blue
|
|
||||||
opacity: 0.8
|
|
||||||
}
|
|
||||||
}
|
|
||||||
b: {
|
|
||||||
shape: rectangle
|
|
||||||
style: {
|
|
||||||
fill: red
|
|
||||||
opacity: 0.5
|
|
||||||
}
|
|
||||||
}
|
|
||||||
c: {
|
|
||||||
shape: diamond
|
|
||||||
style.fill: green
|
|
||||||
style.opacity: 0.8
|
|
||||||
}
|
|
||||||
|
|
||||||
(* -> *)[*]: {
|
|
||||||
&src.style.fill: blue
|
|
||||||
style.stroke-dash: 3
|
|
||||||
}
|
|
||||||
(* -> *)[*]: {
|
|
||||||
&dst.style.opacity: 0.8
|
|
||||||
style.stroke: cyan
|
|
||||||
}
|
|
||||||
(* -> *)[*]: {
|
|
||||||
&src.shape: rectangle
|
|
||||||
&dst.style.fill: green
|
|
||||||
style.stroke-width: 5
|
|
||||||
}
|
|
||||||
|
|
||||||
a -> b
|
|
||||||
b -> c
|
|
||||||
a -> c
|
|
||||||
`, ``)
|
|
||||||
|
|
||||||
tassert.Equal(t, 3, len(g.Edges))
|
|
||||||
|
|
||||||
tassert.Equal(t, "a", g.Edges[0].Src.ID)
|
|
||||||
tassert.Equal(t, "b", g.Edges[0].Dst.ID)
|
|
||||||
tassert.Equal(t, "3", g.Edges[0].Style.StrokeDash.Value)
|
|
||||||
tassert.Equal(t, (*d2graph.Scalar)(nil), g.Edges[0].Style.Stroke)
|
|
||||||
tassert.Equal(t, (*d2graph.Scalar)(nil), g.Edges[0].Style.StrokeWidth)
|
|
||||||
|
|
||||||
tassert.Equal(t, "b", g.Edges[1].Src.ID)
|
|
||||||
tassert.Equal(t, "c", g.Edges[1].Dst.ID)
|
|
||||||
tassert.Equal(t, "cyan", g.Edges[1].Style.Stroke.Value)
|
|
||||||
tassert.Equal(t, (*d2graph.Scalar)(nil), g.Edges[1].Style.StrokeDash)
|
|
||||||
tassert.Equal(t, "5", g.Edges[1].Style.StrokeWidth.Value)
|
|
||||||
|
|
||||||
tassert.Equal(t, "a", g.Edges[2].Src.ID)
|
|
||||||
tassert.Equal(t, "c", g.Edges[2].Dst.ID)
|
|
||||||
tassert.Equal(t, "3", g.Edges[2].Style.StrokeDash.Value)
|
|
||||||
tassert.Equal(t, "cyan", g.Edges[2].Style.Stroke.Value)
|
|
||||||
tassert.Equal(t, (*d2graph.Scalar)(nil), g.Edges[2].Style.StrokeWidth)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "md-shape",
|
|
||||||
run: func(t *testing.T) {
|
|
||||||
g, _ := assertCompile(t, `
|
|
||||||
a.shape: circle
|
|
||||||
a: |md #hi |
|
|
||||||
|
|
||||||
b.shape: circle
|
|
||||||
b.label: |md #hi |
|
|
||||||
|
|
||||||
c: |md #hi |
|
|
||||||
c.shape: circle
|
|
||||||
|
|
||||||
d.label: |md #hi |
|
|
||||||
d.shape: circle
|
|
||||||
|
|
||||||
e: {
|
|
||||||
shape: circle
|
|
||||||
label: |md #hi |
|
|
||||||
}
|
|
||||||
`, ``)
|
|
||||||
tassert.Equal(t, 5, len(g.Objects))
|
|
||||||
for _, obj := range g.Objects {
|
|
||||||
tassert.Equal(t, "circle", obj.Shape.Value, "Object "+obj.ID+" should have circle shape")
|
|
||||||
tassert.Equal(t, "markdown", obj.Language, "Object "+obj.ID+" should have md language")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range tca {
|
for _, tc := range tca {
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ import (
|
||||||
|
|
||||||
"oss.terrastruct.com/util-go/go2"
|
"oss.terrastruct.com/util-go/go2"
|
||||||
|
|
||||||
"oss.terrastruct.com/d2/d2ast"
|
|
||||||
"oss.terrastruct.com/d2/d2graph"
|
"oss.terrastruct.com/d2/d2graph"
|
||||||
"oss.terrastruct.com/d2/d2parser"
|
"oss.terrastruct.com/d2/d2parser"
|
||||||
"oss.terrastruct.com/d2/d2renderers/d2fonts"
|
"oss.terrastruct.com/d2/d2renderers/d2fonts"
|
||||||
|
|
@ -16,7 +15,6 @@ import (
|
||||||
"oss.terrastruct.com/d2/d2themes"
|
"oss.terrastruct.com/d2/d2themes"
|
||||||
"oss.terrastruct.com/d2/lib/color"
|
"oss.terrastruct.com/d2/lib/color"
|
||||||
"oss.terrastruct.com/d2/lib/geo"
|
"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) {
|
func Export(ctx context.Context, g *d2graph.Graph, fontFamily *d2fonts.FontFamily) (*d2target.Diagram, error) {
|
||||||
|
|
@ -47,26 +45,6 @@ func Export(ctx context.Context, g *d2graph.Graph, fontFamily *d2fonts.FontFamil
|
||||||
diagram.Connections[i] = toConnection(g.Edges[i], g.Theme)
|
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
|
return diagram, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -99,46 +77,6 @@ func applyTheme(shape *d2target.Shape, obj *d2graph.Object, theme *d2themes.Them
|
||||||
if theme.SpecialRules.Mono {
|
if theme.SpecialRules.Mono {
|
||||||
shape.FontFamily = "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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -194,9 +132,6 @@ func applyStyles(shape *d2target.Shape, obj *d2graph.Object) {
|
||||||
if obj.Style.DoubleBorder != nil {
|
if obj.Style.DoubleBorder != nil {
|
||||||
shape.DoubleBorder, _ = strconv.ParseBool(obj.Style.DoubleBorder.Value)
|
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 {
|
func toShape(obj *d2graph.Object, g *d2graph.Graph) d2target.Shape {
|
||||||
|
|
@ -209,7 +144,6 @@ func toShape(obj *d2graph.Object, g *d2graph.Graph) d2target.Shape {
|
||||||
shape.Pos = d2target.NewPoint(int(obj.TopLeft.X), int(obj.TopLeft.Y))
|
shape.Pos = d2target.NewPoint(int(obj.TopLeft.X), int(obj.TopLeft.Y))
|
||||||
shape.Width = int(obj.Width)
|
shape.Width = int(obj.Width)
|
||||||
shape.Height = int(obj.Height)
|
shape.Height = int(obj.Height)
|
||||||
shape.Language = obj.Language
|
|
||||||
|
|
||||||
text := obj.Text()
|
text := obj.Text()
|
||||||
shape.Bold = text.IsBold
|
shape.Bold = text.IsBold
|
||||||
|
|
@ -228,18 +162,12 @@ func toShape(obj *d2graph.Object, g *d2graph.Graph) d2target.Shape {
|
||||||
applyStyles(shape, obj)
|
applyStyles(shape, obj)
|
||||||
applyTheme(shape, obj, g.Theme)
|
applyTheme(shape, obj, g.Theme)
|
||||||
shape.Color = text.GetColor(shape.Italic)
|
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)
|
applyStyles(shape, obj)
|
||||||
|
|
||||||
switch strings.ToLower(obj.Shape.Value) {
|
switch obj.Shape.Value {
|
||||||
|
case d2target.ShapeCode, d2target.ShapeText:
|
||||||
|
shape.Language = obj.Language
|
||||||
|
shape.Label = obj.Label.Value
|
||||||
case d2target.ShapeClass:
|
case d2target.ShapeClass:
|
||||||
shape.Class = *obj.Class
|
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
|
// 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
|
||||||
|
|
@ -407,18 +335,7 @@ func toConnection(edge *d2graph.Edge, theme *d2themes.Theme) d2target.Connection
|
||||||
if edge.Tooltip != nil {
|
if edge.Tooltip != nil {
|
||||||
connection.Tooltip = edge.Tooltip.Value
|
connection.Tooltip = edge.Tooltip.Value
|
||||||
}
|
}
|
||||||
if edge.Icon != nil {
|
|
||||||
connection.Icon = edge.Icon
|
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 {
|
if edge.Style.Italic != nil {
|
||||||
connection.Italic, _ = strconv.ParseBool(edge.Style.Italic.Value)
|
connection.Italic, _ = strconv.ParseBool(edge.Style.Italic.Value)
|
||||||
|
|
@ -466,17 +383,5 @@ func toConnection(edge *d2graph.Edge, theme *d2themes.Theme) d2target.Connection
|
||||||
connection.Src = edge.Src.AbsID()
|
connection.Src = edge.Src.AbsID()
|
||||||
connection.Dst = edge.Dst.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
|
return *connection
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,6 @@ func TestExport(t *testing.T) {
|
||||||
t.Run("connection", testConnection)
|
t.Run("connection", testConnection)
|
||||||
t.Run("label", testLabel)
|
t.Run("label", testLabel)
|
||||||
t.Run("theme", testTheme)
|
t.Run("theme", testTheme)
|
||||||
t.Run("legend", testLegend)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func testShape(t *testing.T) {
|
func testShape(t *testing.T) {
|
||||||
|
|
@ -205,30 +204,6 @@ func testTheme(t *testing.T) {
|
||||||
runa(t, tcs)
|
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) {
|
func runa(t *testing.T, tcs []testCase) {
|
||||||
for _, tc := range tcs {
|
for _, tc := range tcs {
|
||||||
tc := tc
|
tc := tc
|
||||||
|
|
@ -328,10 +303,10 @@ a -> b
|
||||||
db, err := compile(ctx, bString)
|
db, err := compile(ctx, bString)
|
||||||
assert.JSON(t, nil, err)
|
assert.JSON(t, nil, err)
|
||||||
|
|
||||||
hashA, err := da.HashID(nil)
|
hashA, err := da.HashID()
|
||||||
assert.JSON(t, nil, err)
|
assert.JSON(t, nil, err)
|
||||||
|
|
||||||
hashB, err := db.HashID(nil)
|
hashB, err := db.HashID()
|
||||||
assert.JSON(t, nil, err)
|
assert.JSON(t, nil, err)
|
||||||
|
|
||||||
assert.NotEqual(t, hashA, hashB)
|
assert.NotEqual(t, hashA, hashB)
|
||||||
|
|
|
||||||
|
|
@ -42,12 +42,6 @@ func (p *printer) node(n d2ast.Node) {
|
||||||
p.blockComment(n)
|
p.blockComment(n)
|
||||||
case *d2ast.Null:
|
case *d2ast.Null:
|
||||||
p.sb.WriteString("null")
|
p.sb.WriteString("null")
|
||||||
case *d2ast.Suspension:
|
|
||||||
if n.Value {
|
|
||||||
p.sb.WriteString("suspend")
|
|
||||||
} else {
|
|
||||||
p.sb.WriteString("unsuspend")
|
|
||||||
}
|
|
||||||
case *d2ast.Boolean:
|
case *d2ast.Boolean:
|
||||||
p.sb.WriteString(strconv.FormatBool(n.Value))
|
p.sb.WriteString(strconv.FormatBool(n.Value))
|
||||||
case *d2ast.Number:
|
case *d2ast.Number:
|
||||||
|
|
@ -127,7 +121,7 @@ func (p *printer) blockComment(bc *d2ast.BlockComment) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *printer) interpolationBoxes(boxes []d2ast.InterpolationBox, isDoubleString bool) {
|
func (p *printer) interpolationBoxes(boxes []d2ast.InterpolationBox, isDoubleString bool) {
|
||||||
for i, b := range boxes {
|
for _, b := range boxes {
|
||||||
if b.Substitution != nil {
|
if b.Substitution != nil {
|
||||||
p.substitution(b.Substitution)
|
p.substitution(b.Substitution)
|
||||||
continue
|
continue
|
||||||
|
|
@ -140,11 +134,6 @@ func (p *printer) interpolationBoxes(boxes []d2ast.InterpolationBox, isDoubleStr
|
||||||
s = escapeUnquotedValue(*b.String, p.inKey)
|
s = escapeUnquotedValue(*b.String, p.inKey)
|
||||||
}
|
}
|
||||||
b.StringRaw = &s
|
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 !isDoubleString {
|
||||||
if _, ok := d2ast.ReservedKeywords[strings.ToLower(*b.StringRaw)]; ok {
|
if _, ok := d2ast.ReservedKeywords[strings.ToLower(*b.StringRaw)]; ok {
|
||||||
|
|
|
||||||
|
|
@ -892,19 +892,6 @@ scenarios: {}
|
||||||
steps: asdf
|
steps: asdf
|
||||||
`,
|
`,
|
||||||
exp: `k
|
exp: `k
|
||||||
`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "vars",
|
|
||||||
in: `vars: {
|
|
||||||
a: "a"
|
|
||||||
b: "X${a})"
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
exp: `vars: {
|
|
||||||
a: "a"
|
|
||||||
b: "X${a})"
|
|
||||||
}
|
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,6 @@ type Graph struct {
|
||||||
BaseAST *d2ast.Map `json:"-"`
|
BaseAST *d2ast.Map `json:"-"`
|
||||||
|
|
||||||
Root *Object `json:"root"`
|
Root *Object `json:"root"`
|
||||||
Legend *Legend `json:"legend,omitempty"`
|
|
||||||
Edges []*Edge `json:"edges"`
|
Edges []*Edge `json:"edges"`
|
||||||
Objects []*Object `json:"objects"`
|
Objects []*Object `json:"objects"`
|
||||||
|
|
||||||
|
|
@ -68,11 +67,6 @@ type Graph struct {
|
||||||
Data map[string]interface{} `json:"data,omitempty"`
|
Data map[string]interface{} `json:"data,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Legend struct {
|
|
||||||
Objects []*Object `json:"objects,omitempty"`
|
|
||||||
Edges []*Edge `json:"edges,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewGraph() *Graph {
|
func NewGraph() *Graph {
|
||||||
d := &Graph{}
|
d := &Graph{}
|
||||||
d.Root = &Object{
|
d.Root = &Object{
|
||||||
|
|
@ -198,7 +192,6 @@ type Attributes struct {
|
||||||
|
|
||||||
Style Style `json:"style"`
|
Style Style `json:"style"`
|
||||||
Icon *url.URL `json:"icon,omitempty"`
|
Icon *url.URL `json:"icon,omitempty"`
|
||||||
IconStyle Style `json:"iconStyle"`
|
|
||||||
Tooltip *Scalar `json:"tooltip,omitempty"`
|
Tooltip *Scalar `json:"tooltip,omitempty"`
|
||||||
Link *Scalar `json:"link,omitempty"`
|
Link *Scalar `json:"link,omitempty"`
|
||||||
|
|
||||||
|
|
@ -571,7 +564,7 @@ func (obj *Object) GetFill() string {
|
||||||
return color.AB5
|
return color.AB5
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.EqualFold(shape, d2target.ShapePerson) || strings.EqualFold(shape, d2target.ShapeC4Person) {
|
if strings.EqualFold(shape, d2target.ShapePerson) {
|
||||||
return color.B3
|
return color.B3
|
||||||
}
|
}
|
||||||
if strings.EqualFold(shape, d2target.ShapeDiamond) {
|
if strings.EqualFold(shape, d2target.ShapeDiamond) {
|
||||||
|
|
@ -717,6 +710,9 @@ func (obj *Object) newObject(ids d2ast.String) *Object {
|
||||||
Label: Scalar{
|
Label: Scalar{
|
||||||
Value: idval,
|
Value: idval,
|
||||||
},
|
},
|
||||||
|
Shape: Scalar{
|
||||||
|
Value: d2target.ShapeRectangle,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
Graph: obj.Graph,
|
Graph: obj.Graph,
|
||||||
|
|
@ -949,16 +945,14 @@ func (obj *Object) GetLabelSize(mtexts []*d2target.MText, ruler *textmeasure.Rul
|
||||||
|
|
||||||
var dims *d2target.TextDimensions
|
var dims *d2target.TextDimensions
|
||||||
switch shapeType {
|
switch shapeType {
|
||||||
case d2target.ShapeClass:
|
case d2target.ShapeText:
|
||||||
dims = GetTextDimensions(mtexts, ruler, obj.Text(), go2.Pointer(d2fonts.SourceCodePro))
|
|
||||||
default:
|
|
||||||
if obj.Language == "latex" {
|
if obj.Language == "latex" {
|
||||||
width, height, err := d2latex.Measure(obj.Text().Text)
|
width, height, err := d2latex.Measure(obj.Text().Text)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
dims = d2target.NewTextDimensions(width, height)
|
dims = d2target.NewTextDimensions(width, height)
|
||||||
} else if obj.Language != "" && shapeType != d2target.ShapeCode {
|
} else if obj.Language != "" {
|
||||||
var err error
|
var err error
|
||||||
dims, err = getMarkdownDimensions(mtexts, ruler, obj.Text(), fontFamily)
|
dims, err = getMarkdownDimensions(mtexts, ruler, obj.Text(), fontFamily)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -967,6 +961,12 @@ func (obj *Object) GetLabelSize(mtexts []*d2target.MText, ruler *textmeasure.Rul
|
||||||
} else {
|
} else {
|
||||||
dims = GetTextDimensions(mtexts, ruler, obj.Text(), fontFamily)
|
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 == "" {
|
if shapeType == d2target.ShapeSQLTable && obj.Label.Value == "" {
|
||||||
|
|
|
||||||
424
d2ir/compile.go
|
|
@ -81,7 +81,6 @@ func Compile(ast *d2ast.Map, opts *CompileOptions) (*Map, []string, error) {
|
||||||
c.compileMap(m, ast, ast)
|
c.compileMap(m, ast, ast)
|
||||||
c.compileSubstitutions(m, nil)
|
c.compileSubstitutions(m, nil)
|
||||||
c.overlayClasses(m)
|
c.overlayClasses(m)
|
||||||
m.removeSuspendedFields()
|
|
||||||
if !c.err.Empty() {
|
if !c.err.Empty() {
|
||||||
return nil, nil, c.err
|
return nil, nil, c.err
|
||||||
}
|
}
|
||||||
|
|
@ -113,7 +112,7 @@ func (c *compiler) overlayClasses(m *Map) {
|
||||||
if lClasses == nil {
|
if lClasses == nil {
|
||||||
lClasses = classes.Copy(l).(*Field)
|
lClasses = classes.Copy(l).(*Field)
|
||||||
l.Fields = append(l.Fields, lClasses)
|
l.Fields = append(l.Fields, lClasses)
|
||||||
} else if lClasses.Map() != nil {
|
} else {
|
||||||
base := classes.Copy(l).(*Field)
|
base := classes.Copy(l).(*Field)
|
||||||
OverlayMap(base.Map(), lClasses.Map())
|
OverlayMap(base.Map(), lClasses.Map())
|
||||||
l.DeleteField("classes")
|
l.DeleteField("classes")
|
||||||
|
|
@ -280,19 +279,6 @@ func (c *compiler) resolveSubstitutions(varsStack []*Map, node Node) (removedFie
|
||||||
break
|
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 {
|
if resolvedField.Primary() == nil {
|
||||||
|
|
@ -380,11 +366,6 @@ func (c *compiler) collectVariables(vars *Map, variables map[string]string) {
|
||||||
if f.Primary() != nil {
|
if f.Primary() != nil {
|
||||||
variables[f.Name.ScalarString()] = f.Primary().Value.ScalarString()
|
variables[f.Name.ScalarString()] = f.Primary().Value.ScalarString()
|
||||||
} else if f.Map() != nil {
|
} 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)
|
c.collectVariables(f.Map(), variables)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -604,20 +585,6 @@ func (c *compiler) compileMap(dst *Map, ast, scopeAST *d2ast.Map) {
|
||||||
c.ensureGlobContext(gctx2.refctx)
|
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())
|
OverlayMap(dst, impn.Map())
|
||||||
impDir := n.Import.Dir()
|
impDir := n.Import.Dir()
|
||||||
c.extendLinks(dst, ParentField(dst), impDir)
|
c.extendLinks(dst, ParentField(dst), impDir)
|
||||||
|
|
@ -726,77 +693,13 @@ func (c *compiler) ampersandFilter(refctx *RefContext) bool {
|
||||||
return true
|
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)
|
fa, err := refctx.ScopeMap.EnsureField(refctx.Key.Key, refctx, false, c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.err.Errors = append(c.err.Errors, err.(d2ast.Error))
|
c.err.Errors = append(c.err.Errors, err.(d2ast.Error))
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if len(fa) == 0 {
|
if len(fa) == 0 {
|
||||||
if refctx.Key.Value.ScalarBox().Unbox() != nil && refctx.Key.Value.ScalarBox().Unbox().ScalarString() == "*" {
|
if refctx.Key.Value.ScalarBox().Unbox().ScalarString() == "*" {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
// The field/edge has no value for this filter
|
// The field/edge has no value for this filter
|
||||||
|
|
@ -847,7 +750,6 @@ func (c *compiler) ampersandFilter(refctx *RefContext) bool {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return c._ampersandFilter(f, refctx)
|
return c._ampersandFilter(f, refctx)
|
||||||
|
|
||||||
case "label":
|
case "label":
|
||||||
f := &Field{}
|
f := &Field{}
|
||||||
n := refctx.ScopeMap.Parent()
|
n := refctx.ScopeMap.Parent()
|
||||||
|
|
@ -866,85 +768,7 @@ func (c *compiler) ampersandFilter(refctx *RefContext) bool {
|
||||||
f.Primary_ = n.Primary()
|
f.Primary_ = n.Primary()
|
||||||
}
|
}
|
||||||
return c._ampersandFilter(f, refctx)
|
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:
|
default:
|
||||||
parent := refctx.ScopeMap.Parent()
|
|
||||||
if field, ok := parent.(*Field); ok {
|
|
||||||
propName := refctx.Key.Key.Last().ScalarString()
|
|
||||||
value := refctx.Key.Value.ScalarBox().Unbox().ScalarString()
|
|
||||||
return c._ampersandPropertyFilter(propName, value, field, refctx.Key)
|
|
||||||
}
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -957,70 +781,6 @@ func (c *compiler) ampersandFilter(refctx *RefContext) bool {
|
||||||
return true
|
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 {
|
func (c *compiler) _ampersandFilter(f *Field, refctx *RefContext) bool {
|
||||||
if refctx.Key.Value.ScalarBox().Unbox() == nil {
|
if refctx.Key.Value.ScalarBox().Unbox() == nil {
|
||||||
c.errorf(refctx.Key, "glob filters cannot be composites")
|
c.errorf(refctx.Key, "glob filters cannot be composites")
|
||||||
|
|
@ -1079,17 +839,6 @@ func (c *compiler) _compileField(f *Field, refctx *RefContext) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 refctx.Key.Primary.Unbox() != nil {
|
||||||
if c.ignoreLazyGlob(f) {
|
if c.ignoreLazyGlob(f) {
|
||||||
return
|
return
|
||||||
|
|
@ -1149,11 +898,6 @@ func (c *compiler) _compileField(f *Field, refctx *RefContext) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
n.(Importable).SetImportAST(refctx.Key.Value.Import)
|
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) {
|
switch n := n.(type) {
|
||||||
case *Field:
|
case *Field:
|
||||||
if n.Primary_ != nil {
|
if n.Primary_ != nil {
|
||||||
|
|
@ -1190,22 +934,6 @@ func (c *compiler) _compileField(f *Field, refctx *RefContext) {
|
||||||
c.overlayClasses(f.Map())
|
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 {
|
} else if refctx.Key.Value.ScalarBox().Unbox() != nil {
|
||||||
if c.ignoreLazyGlob(f) {
|
if c.ignoreLazyGlob(f) {
|
||||||
return
|
return
|
||||||
|
|
@ -1238,10 +966,6 @@ func (c *compiler) extendLinks(m *Map, importF *Field, importDir string) {
|
||||||
nodeBoardKind := NodeBoardKind(m)
|
nodeBoardKind := NodeBoardKind(m)
|
||||||
importIDA := IDA(importF)
|
importIDA := IDA(importF)
|
||||||
for _, f := range m.Fields {
|
for _, f := range m.Fields {
|
||||||
// A substitute or such
|
|
||||||
if f.Name == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if f.Name.ScalarString() == "link" && f.Name.IsUnquoted() {
|
if f.Name.ScalarString() == "link" && f.Name.IsUnquoted() {
|
||||||
if nodeBoardKind != "" {
|
if nodeBoardKind != "" {
|
||||||
c.errorf(f.LastRef().AST(), "a board itself cannot be linked; only objects within a board can be linked")
|
c.errorf(f.LastRef().AST(), "a board itself cannot be linked; only objects within a board can be linked")
|
||||||
|
|
@ -1250,7 +974,7 @@ func (c *compiler) extendLinks(m *Map, importF *Field, importDir string) {
|
||||||
val := f.Primary().Value.ScalarString()
|
val := f.Primary().Value.ScalarString()
|
||||||
|
|
||||||
u, err := url.Parse(html.UnescapeString(val))
|
u, err := url.Parse(html.UnescapeString(val))
|
||||||
isRemote := err == nil && (u.Scheme != "" || strings.HasPrefix(u.Path, "/"))
|
isRemote := err == nil && u.Scheme != ""
|
||||||
if isRemote {
|
if isRemote {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
@ -1288,7 +1012,7 @@ func (c *compiler) extendLinks(m *Map, importF *Field, importDir string) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
u, err := url.Parse(html.UnescapeString(val))
|
u, err := url.Parse(html.UnescapeString(val))
|
||||||
isRemoteImg := err == nil && (u.Scheme != "" || strings.HasPrefix(u.Path, "/"))
|
isRemoteImg := err == nil && u.Scheme != ""
|
||||||
if isRemoteImg {
|
if isRemoteImg {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
@ -1319,6 +1043,11 @@ func (c *compiler) compileLink(f *Field, refctx *RefContext) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if linkIDA[0].ScalarString() == "root" && linkIDA[0].IsUnquoted() {
|
||||||
|
c.errorf(refctx.Key.Key, "cannot refer to root in link")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if !linkIDA[0].IsUnquoted() {
|
if !linkIDA[0].IsUnquoted() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -1413,99 +1142,6 @@ func (c *compiler) _compileEdges(refctx *RefContext) {
|
||||||
refctx.ScopeMap.DeleteEdge(e.ID)
|
refctx.ScopeMap.DeleteEdge(e.ID)
|
||||||
continue
|
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{
|
e.References = append(e.References, &EdgeReference{
|
||||||
Context_: refctx,
|
Context_: refctx,
|
||||||
DueToGlob_: len(c.globRefContextStack) > 0,
|
DueToGlob_: len(c.globRefContextStack) > 0,
|
||||||
|
|
@ -1532,7 +1168,7 @@ func (c *compiler) _compileEdges(refctx *RefContext) {
|
||||||
}
|
}
|
||||||
c.compileField(e.Map_, refctx.Key.EdgeKey, refctx)
|
c.compileField(e.Map_, refctx.Key.EdgeKey, refctx)
|
||||||
} else {
|
} else {
|
||||||
if refctx.Key.Primary.Unbox() != nil && refctx.Key.Primary.Suspension == nil {
|
if refctx.Key.Primary.Unbox() != nil {
|
||||||
if c.ignoreLazyGlob(e) {
|
if c.ignoreLazyGlob(e) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -1553,7 +1189,7 @@ func (c *compiler) _compileEdges(refctx *RefContext) {
|
||||||
c.mapRefContextStack = append(c.mapRefContextStack, refctx)
|
c.mapRefContextStack = append(c.mapRefContextStack, refctx)
|
||||||
c.compileMap(e.Map_, refctx.Key.Value.Map, refctx.ScopeAST)
|
c.compileMap(e.Map_, refctx.Key.Value.Map, refctx.ScopeAST)
|
||||||
c.mapRefContextStack = c.mapRefContextStack[:len(c.mapRefContextStack)-1]
|
c.mapRefContextStack = c.mapRefContextStack[:len(c.mapRefContextStack)-1]
|
||||||
} else if refctx.Key.Value.ScalarBox().Unbox() != nil && refctx.Key.Value.Suspension == nil {
|
} else if refctx.Key.Value.ScalarBox().Unbox() != nil {
|
||||||
if c.ignoreLazyGlob(e) {
|
if c.ignoreLazyGlob(e) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -1624,46 +1260,8 @@ func (c *compiler) compileArray(dst *Array, a *d2ast.Array, scopeAST *d2ast.Map)
|
||||||
Value: []d2ast.InterpolationBox{{Substitution: an.Substitution}},
|
Value: []d2ast.InterpolationBox{{Substitution: an.Substitution}},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
case *d2ast.Comment:
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dst.Values = append(dst.Values, irv)
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -593,7 +593,7 @@ classes: {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`)
|
`)
|
||||||
assert.ErrorString(t, err, `TestCompile/classes/nonroot.d2:2:3: classes must be declared at a board root scope`)
|
assert.ErrorString(t, err, `TestCompile/classes/nonroot.d2:2:3: classes is only allowed at a board root`)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
119
d2ir/d2ir.go
|
|
@ -318,7 +318,6 @@ type Field struct {
|
||||||
// *Map.
|
// *Map.
|
||||||
parent Node
|
parent Node
|
||||||
importAST d2ast.Node
|
importAST d2ast.Node
|
||||||
suspended bool
|
|
||||||
|
|
||||||
Name d2ast.String `json:"name"`
|
Name d2ast.String `json:"name"`
|
||||||
|
|
||||||
|
|
@ -489,7 +488,6 @@ type Edge struct {
|
||||||
// *Map
|
// *Map
|
||||||
parent Node
|
parent Node
|
||||||
importAST d2ast.Node
|
importAST d2ast.Node
|
||||||
suspended bool
|
|
||||||
|
|
||||||
ID *EdgeID `json:"edge_id"`
|
ID *EdgeID `json:"edge_id"`
|
||||||
|
|
||||||
|
|
@ -650,41 +648,7 @@ func (rc *RefContext) EdgeIndex() int {
|
||||||
func (rc *RefContext) Equal(rc2 *RefContext) bool {
|
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.
|
// 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.
|
// Same with ScopeMap.
|
||||||
if !(rc.Key.Equals(rc2.Key) && rc.Scope == rc2.Scope && rc.ScopeAST == rc2.ScopeAST) {
|
return 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 {
|
func (m *Map) FieldCountRecursive() int {
|
||||||
|
|
@ -705,41 +669,10 @@ func (m *Map) FieldCountRecursive() int {
|
||||||
return acc
|
return acc
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *compiler) IsContainer(m *Map) bool {
|
func (m *Map) IsContainer() bool {
|
||||||
if m == nil {
|
if m == nil {
|
||||||
return false
|
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 {
|
for _, f := range m.Fields {
|
||||||
_, isReserved := d2ast.ReservedKeywords[f.Name.ScalarString()]
|
_, isReserved := d2ast.ReservedKeywords[f.Name.ScalarString()]
|
||||||
if !(isReserved && f.Name.IsUnquoted()) {
|
if !(isReserved && f.Name.IsUnquoted()) {
|
||||||
|
|
@ -808,11 +741,9 @@ func (m *Map) getField(ida []d2ast.String) *Field {
|
||||||
if !strings.EqualFold(f.Name.ScalarString(), s.ScalarString()) {
|
if !strings.EqualFold(f.Name.ScalarString(), s.ScalarString()) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if _, isReserved := d2ast.ReservedKeywords[strings.ToLower(s.ScalarString())]; isReserved {
|
|
||||||
if f.Name.IsUnquoted() != s.IsUnquoted() {
|
if f.Name.IsUnquoted() != s.IsUnquoted() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if len(rest) == 0 {
|
if len(rest) == 0 {
|
||||||
return f
|
return f
|
||||||
}
|
}
|
||||||
|
|
@ -956,22 +887,17 @@ func (m *Map) ensureField(i int, kp *d2ast.KeyPath, refctx *RefContext, create b
|
||||||
}
|
}
|
||||||
|
|
||||||
if headString == "classes" && head.IsUnquoted() && NodeBoardKind(m) == "" {
|
if headString == "classes" && head.IsUnquoted() && NodeBoardKind(m) == "" {
|
||||||
return d2parser.Errorf(kp.Path[i].Unbox(), "%s must be declared at a board root scope", headString)
|
return d2parser.Errorf(kp.Path[i].Unbox(), "%s is only allowed at a board root", headString)
|
||||||
}
|
}
|
||||||
|
|
||||||
if findBoardKeyword(head) != -1 && head.IsUnquoted() && NodeBoardKind(m) == "" {
|
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)
|
return d2parser.Errorf(kp.Path[i].Unbox(), "%s is only allowed at a board root", headString)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, f := range m.Fields {
|
for _, f := range m.Fields {
|
||||||
if !(f.Name != nil && strings.EqualFold(f.Name.ScalarString(), head.ScalarString())) {
|
if !(f.Name != nil && strings.EqualFold(f.Name.ScalarString(), head.ScalarString()) && f.Name.IsUnquoted() == head.IsUnquoted()) {
|
||||||
continue
|
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.
|
// Don't add references for fake common KeyPath from trimCommon in CreateEdge.
|
||||||
if refctx != nil {
|
if refctx != nil {
|
||||||
|
|
@ -1061,25 +987,9 @@ func (m *Map) DeleteEdge(eid *EdgeID) *Edge {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
resolvedEID, resolvedM, common, err := eid.resolve(m)
|
for i, e := range m.Edges {
|
||||||
if err != nil {
|
if e.ID.Match(eid) {
|
||||||
return nil
|
m.Edges = append(m.Edges[:i], m.Edges[i+1:]...)
|
||||||
}
|
|
||||||
|
|
||||||
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
|
return e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1347,7 +1257,7 @@ func (m *Map) createEdge(eid *EdgeID, refctx *RefContext, gctx *globContext, c *
|
||||||
|
|
||||||
if refctx.Edge.Src.HasMultiGlob() {
|
if refctx.Edge.Src.HasMultiGlob() {
|
||||||
// If src has a double glob we only select leafs, those without children.
|
// If src has a double glob we only select leafs, those without children.
|
||||||
if c.IsContainer(src.Map()) {
|
if src.Map().IsContainer() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if NodeBoardKind(src) != "" || ParentBoard(src) != ParentBoard(dst) {
|
if NodeBoardKind(src) != "" || ParentBoard(src) != ParentBoard(dst) {
|
||||||
|
|
@ -1356,7 +1266,7 @@ func (m *Map) createEdge(eid *EdgeID, refctx *RefContext, gctx *globContext, c *
|
||||||
}
|
}
|
||||||
if refctx.Edge.Dst.HasMultiGlob() {
|
if refctx.Edge.Dst.HasMultiGlob() {
|
||||||
// If dst has a double glob we only select leafs, those without children.
|
// If dst has a double glob we only select leafs, those without children.
|
||||||
if c.IsContainer(dst.Map()) {
|
if dst.Map().IsContainer() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if NodeBoardKind(dst) != "" || ParentBoard(src) != ParentBoard(dst) {
|
if NodeBoardKind(dst) != "" || ParentBoard(src) != ParentBoard(dst) {
|
||||||
|
|
@ -1485,14 +1395,7 @@ func (f *Field) AST() d2ast.Node {
|
||||||
k.Primary = d2ast.MakeValueBox(f.Primary_.AST().(d2ast.Value)).ScalarBox()
|
k.Primary = d2ast.MakeValueBox(f.Primary_.AST().(d2ast.Value)).ScalarBox()
|
||||||
}
|
}
|
||||||
if f.Composite != nil {
|
if f.Composite != nil {
|
||||||
value := f.Composite.AST().(d2ast.Value)
|
k.Value = d2ast.MakeValueBox(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
|
return k
|
||||||
|
|
|
||||||
|
|
@ -111,66 +111,11 @@ func (c *compiler) __import(imp *d2ast.Import) (*Map, bool) {
|
||||||
|
|
||||||
c.compileMap(ir, ast, ast)
|
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{}{}
|
c.seenImports[impPath] = struct{}{}
|
||||||
|
|
||||||
return ir, true
|
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) {
|
func nilScopeMap(n Node) {
|
||||||
switch n := n.(type) {
|
switch n := n.(type) {
|
||||||
case *Map:
|
case *Map:
|
||||||
|
|
|
||||||
|
|
@ -19,20 +19,16 @@ import (
|
||||||
"oss.terrastruct.com/d2/d2lsp"
|
"oss.terrastruct.com/d2/d2lsp"
|
||||||
"oss.terrastruct.com/d2/d2oracle"
|
"oss.terrastruct.com/d2/d2oracle"
|
||||||
"oss.terrastruct.com/d2/d2parser"
|
"oss.terrastruct.com/d2/d2parser"
|
||||||
"oss.terrastruct.com/d2/d2renderers/d2animate"
|
|
||||||
"oss.terrastruct.com/d2/d2renderers/d2fonts"
|
"oss.terrastruct.com/d2/d2renderers/d2fonts"
|
||||||
"oss.terrastruct.com/d2/d2renderers/d2svg"
|
"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/log"
|
||||||
"oss.terrastruct.com/d2/lib/memfs"
|
"oss.terrastruct.com/d2/lib/memfs"
|
||||||
"oss.terrastruct.com/d2/lib/textmeasure"
|
"oss.terrastruct.com/d2/lib/textmeasure"
|
||||||
"oss.terrastruct.com/d2/lib/urlenc"
|
"oss.terrastruct.com/d2/lib/urlenc"
|
||||||
"oss.terrastruct.com/d2/lib/version"
|
"oss.terrastruct.com/d2/lib/version"
|
||||||
|
"oss.terrastruct.com/util-go/go2"
|
||||||
)
|
)
|
||||||
|
|
||||||
const DEFAULT_INPUT_PATH = "index"
|
|
||||||
|
|
||||||
func GetParentID(args []js.Value) (interface{}, error) {
|
func GetParentID(args []js.Value) (interface{}, error) {
|
||||||
if len(args) < 1 {
|
if len(args) < 1 {
|
||||||
return nil, &WASMError{Message: "missing id argument", Code: 400}
|
return nil, &WASMError{Message: "missing id argument", Code: 400}
|
||||||
|
|
@ -124,14 +120,8 @@ func GetELKGraph(args []js.Value) (interface{}, error) {
|
||||||
return nil, &WASMError{Message: "missing 'fs' field in input JSON", Code: 400}
|
return nil, &WASMError{Message: "missing 'fs' field in input JSON", Code: 400}
|
||||||
}
|
}
|
||||||
|
|
||||||
inputPath := DEFAULT_INPUT_PATH
|
if _, ok := input.FS["index"]; !ok {
|
||||||
|
return nil, &WASMError{Message: "missing 'index' file in input fs", Code: 400}
|
||||||
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)
|
fs, err := memfs.New(input.FS)
|
||||||
|
|
@ -139,7 +129,7 @@ func GetELKGraph(args []js.Value) (interface{}, error) {
|
||||||
return nil, &WASMError{Message: fmt.Sprintf("invalid fs input: %s", err.Error()), Code: 400}
|
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{
|
g, _, err := d2compiler.Compile("", strings.NewReader(input.FS["index"]), &d2compiler.CompileOptions{
|
||||||
UTF16Pos: true,
|
UTF16Pos: true,
|
||||||
FS: fs,
|
FS: fs,
|
||||||
})
|
})
|
||||||
|
|
@ -176,122 +166,64 @@ func Compile(args []js.Value) (interface{}, error) {
|
||||||
return nil, &WASMError{Message: "missing 'fs' field in input JSON", Code: 400}
|
return nil, &WASMError{Message: "missing 'fs' field in input JSON", Code: 400}
|
||||||
}
|
}
|
||||||
|
|
||||||
compileOpts := &d2lib.CompileOptions{
|
if _, ok := input.FS["index"]; !ok {
|
||||||
UTF16Pos: true,
|
return nil, &WASMError{Message: "missing 'index' file in input fs", Code: 400}
|
||||||
}
|
}
|
||||||
|
|
||||||
inputPath := DEFAULT_INPUT_PATH
|
fs, err := memfs.New(input.FS)
|
||||||
|
|
||||||
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 {
|
if err != nil {
|
||||||
return nil, &WASMError{Message: fmt.Sprintf("invalid fs input: %s", err.Error()), Code: 400}
|
return nil, &WASMError{Message: fmt.Sprintf("invalid fs input: %s", err.Error()), Code: 400}
|
||||||
}
|
}
|
||||||
|
|
||||||
var fontRegular []byte
|
ruler, err := textmeasure.NewRuler()
|
||||||
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 {
|
if err != nil {
|
||||||
return nil, &WASMError{Message: fmt.Sprintf("text ruler cannot be initialized: %s", err.Error()), Code: 500}
|
return nil, &WASMError{Message: fmt.Sprintf("text ruler cannot be initialized: %s", err.Error()), Code: 500}
|
||||||
}
|
}
|
||||||
|
ctx := log.WithDefault(context.Background())
|
||||||
|
layoutFunc := d2dagrelayout.DefaultLayout
|
||||||
if input.Opts != nil && input.Opts.Layout != nil {
|
if input.Opts != nil && input.Opts.Layout != nil {
|
||||||
compileOpts.Layout = input.Opts.Layout
|
switch *input.Opts.Layout {
|
||||||
|
case "dagre":
|
||||||
|
layoutFunc = d2dagrelayout.DefaultLayout
|
||||||
|
case "elk":
|
||||||
|
layoutFunc = d2elklayout.DefaultLayout
|
||||||
|
default:
|
||||||
|
return nil, &WASMError{Message: fmt.Sprintf("layout option '%s' not recognized", *input.Opts.Layout), Code: 400}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
layoutResolver := func(engine string) (d2graph.LayoutGraph, error) {
|
||||||
|
return layoutFunc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
renderOpts := &d2svg.RenderOpts{}
|
renderOpts := &d2svg.RenderOpts{}
|
||||||
if input.Opts != nil && input.Opts.Sketch != nil {
|
var fontFamily *d2fonts.FontFamily
|
||||||
|
if input.Opts != nil && input.Opts.Sketch != nil && *input.Opts.Sketch {
|
||||||
|
fontFamily = go2.Pointer(d2fonts.HandDrawn)
|
||||||
renderOpts.Sketch = input.Opts.Sketch
|
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 {
|
if input.Opts != nil && input.Opts.ThemeID != nil {
|
||||||
renderOpts.ThemeID = input.Opts.ThemeID
|
renderOpts.ThemeID = input.Opts.ThemeID
|
||||||
}
|
}
|
||||||
if input.Opts != nil && input.Opts.DarkThemeID != nil {
|
diagram, g, err := d2lib.Compile(ctx, input.FS["index"], &d2lib.CompileOptions{
|
||||||
renderOpts.DarkThemeID = input.Opts.DarkThemeID
|
UTF16Pos: true,
|
||||||
}
|
FS: fs,
|
||||||
if input.Opts != nil && input.Opts.Scale != nil {
|
Ruler: ruler,
|
||||||
renderOpts.Scale = input.Opts.Scale
|
LayoutResolver: layoutResolver,
|
||||||
}
|
FontFamily: fontFamily,
|
||||||
|
}, renderOpts)
|
||||||
ctx := log.WithDefault(context.Background())
|
|
||||||
diagram, g, err := d2lib.Compile(ctx, input.FS[inputPath], compileOpts, renderOpts)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if pe, ok := err.(*d2parser.ParseError); ok {
|
if pe, ok := err.(*d2parser.ParseError); ok {
|
||||||
errs, _ := json.Marshal(pe.Errors)
|
return nil, &WASMError{Message: pe.Error(), Code: 400}
|
||||||
return nil, &WASMError{Message: string(errs), Code: 400}
|
|
||||||
}
|
}
|
||||||
return nil, &WASMError{Message: err.Error(), Code: 500}
|
return nil, &WASMError{Message: err.Error(), Code: 500}
|
||||||
}
|
}
|
||||||
|
|
||||||
input.FS[inputPath] = d2format.Format(g.AST)
|
input.FS["index"] = d2format.Format(g.AST)
|
||||||
|
|
||||||
return CompileResponse{
|
return CompileResponse{
|
||||||
FS: input.FS,
|
FS: input.FS,
|
||||||
InputPath: inputPath,
|
|
||||||
Diagram: *diagram,
|
Diagram: *diagram,
|
||||||
Graph: *g,
|
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
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -308,159 +240,21 @@ func Render(args []js.Value) (interface{}, error) {
|
||||||
return nil, &WASMError{Message: "missing 'diagram' field in input JSON", Code: 400}
|
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{}
|
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 {
|
if input.Opts != nil && input.Opts.Sketch != nil {
|
||||||
renderOpts.Sketch = input.Opts.Sketch
|
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 {
|
if input.Opts != nil && input.Opts.ThemeID != nil {
|
||||||
renderOpts.ThemeID = input.Opts.ThemeID
|
renderOpts.ThemeID = input.Opts.ThemeID
|
||||||
}
|
}
|
||||||
if input.Opts != nil && input.Opts.DarkThemeID != nil {
|
out, err := d2svg.Render(input.Diagram, renderOpts)
|
||||||
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 {
|
if err != nil {
|
||||||
return nil, &WASMError{Message: fmt.Sprintf("render failed: %s", err.Error()), Code: 500}
|
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
|
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) {
|
func GetBoardAtPosition(args []js.Value) (interface{}, error) {
|
||||||
if len(args) < 3 {
|
if len(args) < 3 {
|
||||||
return nil, &WASMError{Message: "missing required arguments", Code: 400}
|
return nil, &WASMError{Message: "missing required arguments", Code: 400}
|
||||||
|
|
|
||||||
|
|
@ -33,39 +33,19 @@ type BoardPositionResponse struct {
|
||||||
|
|
||||||
type CompileRequest struct {
|
type CompileRequest struct {
|
||||||
FS map[string]string `json:"fs"`
|
FS map[string]string `json:"fs"`
|
||||||
InputPath *string `json:"inputPath"`
|
Opts *RenderOptions `json:"options"`
|
||||||
Opts *CompileOptions `json:"options"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type RenderOptions struct {
|
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"`
|
Layout *string `json:"layout"`
|
||||||
FontRegular *[]byte `json:"FontRegular"`
|
Sketch *bool `json:"sketch"`
|
||||||
FontItalic *[]byte `json:"FontItalic"`
|
ThemeID *int64 `json:"themeID"`
|
||||||
FontBold *[]byte `json:"FontBold"`
|
|
||||||
FontSemibold *[]byte `json:"FontSemibold"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type CompileResponse struct {
|
type CompileResponse struct {
|
||||||
FS map[string]string `json:"fs"`
|
FS map[string]string `json:"fs"`
|
||||||
InputPath string `json:"inputPath"`
|
|
||||||
Diagram d2target.Diagram `json:"diagram"`
|
Diagram d2target.Diagram `json:"diagram"`
|
||||||
Graph d2graph.Graph `json:"graph"`
|
Graph d2graph.Graph `json:"graph"`
|
||||||
RenderOptions RenderOptions `json:"renderOptions"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type CompletionResponse struct {
|
type CompletionResponse struct {
|
||||||
|
|
|
||||||
|
|
@ -3,29 +3,6 @@
|
||||||
All notable changes to only the d2.js package will be documented in this file. **Does not
|
All notable changes to only the d2.js package will be documented in this file. **Does not
|
||||||
include changes to the main d2 project.**
|
include changes to the main d2 project.**
|
||||||
|
|
||||||
## Next
|
## [0.1.0] - 2025-01-12
|
||||||
|
|
||||||
- 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
|
First public release
|
||||||
|
|
|
||||||
|
|
@ -29,24 +29,10 @@ pnpm add @terrastruct/d2
|
||||||
bun add @terrastruct/d2
|
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
|
## Usage
|
||||||
|
|
||||||
D2.js uses webworkers to call a WASM file.
|
D2.js uses webworkers to call a WASM file.
|
||||||
|
|
||||||
### Basic Usage
|
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// Same for Node or browser
|
// Same for Node or browser
|
||||||
import { D2 } from '@terrastruct/d2';
|
import { D2 } from '@terrastruct/d2';
|
||||||
|
|
@ -56,97 +42,24 @@ import { D2 } from '@terrastruct/d2';
|
||||||
const d2 = new D2();
|
const d2 = new D2();
|
||||||
|
|
||||||
const result = await d2.compile('x -> y');
|
const result = await d2.compile('x -> y');
|
||||||
const svg = await d2.render(result.diagram, result.renderOptions);
|
const svg = await d2.render(result.diagram);
|
||||||
```
|
|
||||||
|
|
||||||
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
|
## API Reference
|
||||||
|
|
||||||
### `new D2()`
|
### `new D2()`
|
||||||
|
|
||||||
Creates a new D2 instance.
|
Creates a new D2 instance.
|
||||||
|
|
||||||
### `compile(input: string | CompileRequest, options?: CompileOptions): Promise<CompileResult>`
|
### `compile(input: string, options?: CompileOptions): Promise<CompileResult>`
|
||||||
|
Compiles D2 markup into an intermediate representation.
|
||||||
|
|
||||||
Compiles D2 markup into an intermediate representation. It compile options are provided in both `input` and `options`, the latter will take precedence.
|
Options:
|
||||||
|
- `layout`: Layout engine to use ('dagre' | 'elk') [default: 'dagre']
|
||||||
|
- `sketch`: Enable sketch mode [default: false]
|
||||||
|
|
||||||
### `render(diagram: Diagram, options?: RenderOptions): Promise<string>`
|
### `render(diagram: Diagram, options?: RenderOptions): Promise<string>`
|
||||||
|
|
||||||
Renders a compiled diagram to SVG.
|
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
|
## Development
|
||||||
|
|
||||||
D2.js uses Bun, so install this first.
|
D2.js uses Bun, so install this first.
|
||||||
|
|
@ -174,16 +87,6 @@ You can browse the examples by running the dev server:
|
||||||
|
|
||||||
Visit `http://localhost:3000` to see the example page.
|
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
|
## Contributing
|
||||||
|
|
||||||
Contributions are welcome!
|
Contributions are welcome!
|
||||||
|
|
|
||||||
|
|
@ -17,59 +17,3 @@ fi
|
||||||
|
|
||||||
cd d2js/js
|
cd d2js/js
|
||||||
sh_c bun build.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
|
|
@ -36,7 +36,7 @@
|
||||||
const input = document.getElementById("input").value;
|
const input = document.getElementById("input").value;
|
||||||
try {
|
try {
|
||||||
const result = await d2.compile(input);
|
const result = await d2.compile(input);
|
||||||
const svg = await d2.render(result.diagram, result.renderOptions);
|
const svg = await d2.render(result.diagram);
|
||||||
document.getElementById("output").innerHTML = svg;
|
document.getElementById("output").innerHTML = svg;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|
|
||||||
|
|
@ -10,14 +10,12 @@
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: system-ui, -apple-system, sans-serif;
|
font-family: system-ui, -apple-system, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
.controls {
|
.controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
width: 400px;
|
width: 400px;
|
||||||
}
|
}
|
||||||
|
|
||||||
textarea {
|
textarea {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 300px;
|
height: 300px;
|
||||||
|
|
@ -26,7 +24,6 @@
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
.options-group {
|
.options-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
@ -35,36 +32,23 @@
|
||||||
border: 1px solid #eee;
|
border: 1px solid #eee;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
.layout-toggle,
|
||||||
.option:has(.option-toggle-box:not(:checked)) .option-select {
|
.sketch-toggle {
|
||||||
opacity: 0.5;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.option {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
.radio-group {
|
||||||
.input-label,
|
display: flex;
|
||||||
.checkbox-label,
|
gap: 12px;
|
||||||
.select-label {
|
}
|
||||||
|
.radio-label,
|
||||||
|
.checkbox-label {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
|
||||||
|
|
||||||
.checkbox-label,
|
|
||||||
.select-label {
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-input,
|
|
||||||
.number-input {
|
|
||||||
width: 3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
button {
|
||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
background: #0066cc;
|
background: #0066cc;
|
||||||
|
|
@ -73,11 +57,9 @@
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:hover {
|
button:hover {
|
||||||
background: #0052a3;
|
background: #0052a3;
|
||||||
}
|
}
|
||||||
|
|
||||||
#output {
|
#output {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
|
@ -85,287 +67,35 @@
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#output svg {
|
#output svg {
|
||||||
min-width: 100%;
|
max-width: 100%;
|
||||||
max-height: 90vh;
|
max-height: 90vh;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<textarea id="input">x -> y</textarea>
|
<textarea id="input">x -> y</textarea>
|
||||||
<div class="options-group">
|
<div class="options-group">
|
||||||
<div class="option">
|
<div class="layout-toggle">
|
||||||
<div class="option-toggle">
|
<span>Layout:</span>
|
||||||
<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">
|
<div class="radio-group">
|
||||||
<label class="radio-label">
|
<label class="radio-label">
|
||||||
<input type="radio" name="layout-select" value="dagre" checked />
|
<input type="radio" name="layout" value="dagre" checked />
|
||||||
Dagre
|
Dagre
|
||||||
</label>
|
</label>
|
||||||
<label class="radio-label">
|
<label class="radio-label">
|
||||||
<input type="radio" name="layout-select" value="elk" />
|
<input type="radio" name="layout" value="elk" />
|
||||||
ELK
|
ELK
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="sketch-toggle">
|
||||||
<div class="option">
|
|
||||||
<div class="option-toggle">
|
|
||||||
<label class="checkbox-label">
|
<label class="checkbox-label">
|
||||||
<input type="checkbox" id="sketch-toggle" class="option-toggle-box" />
|
<input type="checkbox" id="sketch" />
|
||||||
<span>Sketch Mode</span>
|
Sketch mode
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
<button onclick="compile()">Compile</button>
|
<button onclick="compile()">Compile</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -373,80 +103,13 @@
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import { D2 } from "../dist/browser/index.js";
|
import { D2 } from "../dist/browser/index.js";
|
||||||
const d2 = new D2();
|
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 () => {
|
window.compile = async () => {
|
||||||
const input = document.getElementById("input").value;
|
const input = document.getElementById("input").value;
|
||||||
const layout = document.getElementById("layout-toggle").checked
|
const layout = document.querySelector('input[name="layout"]:checked').value;
|
||||||
? document.querySelector('input[name="layout-select"]:checked').value
|
const sketch = document.getElementById("sketch").checked;
|
||||||
: 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 {
|
try {
|
||||||
const result = await d2.compile(input, {
|
const result = await d2.compile(input, { layout, sketch });
|
||||||
layout,
|
const svg = await d2.render(result.diagram, { sketch });
|
||||||
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;
|
document.getElementById("output").innerHTML = svg;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|
|
||||||
343
d2js/js/index.d.ts
vendored
|
|
@ -1,343 +0,0 @@
|
||||||
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 */;
|
|
||||||
}
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
"name": "@terrastruct/d2",
|
"name": "@terrastruct/d2",
|
||||||
"author": "Terrastruct, Inc.",
|
"author": "Terrastruct, Inc.",
|
||||||
"description": "D2.js is a wrapper around the WASM build of D2, the modern text-to-diagram language.",
|
"description": "D2.js is a wrapper around the WASM build of D2, the modern text-to-diagram language.",
|
||||||
"version": "0.1.23",
|
"version": "0.1.21",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/terrastruct/d2.git",
|
"url": "git+https://github.com/terrastruct/d2.git",
|
||||||
|
|
@ -23,28 +23,23 @@
|
||||||
"browser": "./dist/browser/index.js",
|
"browser": "./dist/browser/index.js",
|
||||||
"import": {
|
"import": {
|
||||||
"browser": "./dist/browser/index.js",
|
"browser": "./dist/browser/index.js",
|
||||||
"default": "./dist/node-esm/index.js",
|
"default": "./dist/node-esm/index.js"
|
||||||
"types": "./index.d.ts"
|
|
||||||
},
|
|
||||||
"require": {
|
|
||||||
"default": "./dist/node-cjs/index.js",
|
|
||||||
"types": "./index.d.ts"
|
|
||||||
},
|
},
|
||||||
|
"require": "./dist/node-cjs/index.js",
|
||||||
"default": "./dist/node-esm/index.js"
|
"default": "./dist/node-esm/index.js"
|
||||||
},
|
},
|
||||||
"./worker": "./dist/browser/worker.js"
|
"./worker": "./dist/browser/worker.js"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"dist"
|
||||||
"index.d.ts"
|
|
||||||
],
|
],
|
||||||
"types": "./index.d.ts",
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "./make.sh build",
|
"build": "./make.sh build",
|
||||||
"test": "bun test test/unit",
|
"test": "bun test test/unit",
|
||||||
"test:integration": "bun test test/integration",
|
"test:integration": "bun test test/integration",
|
||||||
"test:all": "bun run test && bun run test:integration",
|
"test:all": "bun run test && bun run test:integration",
|
||||||
"dev": "bun --watch dev-server.js"
|
"dev": "bun --watch dev-server.js",
|
||||||
|
"prepublishOnly": "./make.sh all"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"d2",
|
"d2",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,10 @@
|
||||||
import { createWorker, loadFile } from "./platform.js";
|
import { createWorker, loadFile } from "./platform.js";
|
||||||
|
|
||||||
|
const DEFAULT_OPTIONS = {
|
||||||
|
layout: "dagre",
|
||||||
|
sketch: false,
|
||||||
|
};
|
||||||
|
|
||||||
export class D2 {
|
export class D2 {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.ready = this.init();
|
this.ready = this.init();
|
||||||
|
|
@ -81,15 +86,17 @@ export class D2 {
|
||||||
}
|
}
|
||||||
|
|
||||||
async compile(input, options = {}) {
|
async compile(input, options = {}) {
|
||||||
|
const opts = { ...DEFAULT_OPTIONS, ...options };
|
||||||
const request =
|
const request =
|
||||||
typeof input === "string"
|
typeof input === "string"
|
||||||
? { fs: { index: input }, options }
|
? { fs: { index: input }, options: opts }
|
||||||
: { ...input, options: { ...options, ...input.options } };
|
: { ...input, options: { ...opts, ...input.options } };
|
||||||
return this.sendMessage("compile", request);
|
return this.sendMessage("compile", request);
|
||||||
}
|
}
|
||||||
|
|
||||||
async render(diagram, options = {}) {
|
async render(diagram, options = {}) {
|
||||||
return this.sendMessage("render", { diagram, options });
|
const opts = { ...DEFAULT_OPTIONS, ...options };
|
||||||
|
return this.sendMessage("render", { diagram, options: opts });
|
||||||
}
|
}
|
||||||
|
|
||||||
async encode(script) {
|
async encode(script) {
|
||||||
|
|
|
||||||
|
|
@ -30,16 +30,13 @@ export function setupMessageHandler(isNode, port, initWasm) {
|
||||||
// single-threaded WASM call cannot complete without giving control back
|
// single-threaded WASM call cannot complete without giving control back
|
||||||
// So we compute it, store it here, then during elk layout, instead
|
// 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)
|
// 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
|
if (data.options.layout === "elk") {
|
||||||
// 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 elkGraph = await d2.getELKGraph(JSON.stringify(data));
|
||||||
const response = JSON.parse(elkGraph);
|
const elkGraph2 = JSON.parse(elkGraph).data;
|
||||||
if (response.error) throw new Error(response.error.message);
|
|
||||||
const elkGraph2 = response.data;
|
|
||||||
const layout = await elk.layout(elkGraph2);
|
const layout = await elk.layout(elkGraph2);
|
||||||
globalThis.elkResult = layout;
|
globalThis.elkResult = layout;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await d2.compile(JSON.stringify(data));
|
const result = await d2.compile(JSON.stringify(data));
|
||||||
const response = JSON.parse(result);
|
const response = JSON.parse(result);
|
||||||
if (response.error) throw new Error(response.error.message);
|
if (response.error) throw new Error(response.error.message);
|
||||||
|
|
@ -54,10 +51,7 @@ export function setupMessageHandler(isNode, port, initWasm) {
|
||||||
const result = await d2.render(JSON.stringify(data));
|
const result = await d2.render(JSON.stringify(data));
|
||||||
const response = JSON.parse(result);
|
const response = JSON.parse(result);
|
||||||
if (response.error) throw new Error(response.error.message);
|
if (response.error) throw new Error(response.error.message);
|
||||||
const decoded = new TextDecoder().decode(
|
currentPort.postMessage({ type: "result", data: atob(response.data) });
|
||||||
Uint8Array.from(atob(response.data), (c) => c.charCodeAt(0))
|
|
||||||
);
|
|
||||||
currentPort.postMessage({ type: "result", data: decoded });
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
currentPort.postMessage({ type: "error", error: err.message });
|
currentPort.postMessage({ type: "error", error: err.message });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,10 +27,12 @@ export function setupMessageHandler(isNode, port, initWasm) {
|
||||||
|
|
||||||
case "compile":
|
case "compile":
|
||||||
try {
|
try {
|
||||||
|
if (data.options.layout === "elk") {
|
||||||
const elkGraph = await d2.getELKGraph(JSON.stringify(data));
|
const elkGraph = await d2.getELKGraph(JSON.stringify(data));
|
||||||
const elkGraph2 = JSON.parse(elkGraph).data;
|
const elkGraph2 = JSON.parse(elkGraph).data;
|
||||||
const layout = await elk.layout(elkGraph2);
|
const layout = await elk.layout(elkGraph2);
|
||||||
globalThis.elkResult = layout;
|
globalThis.elkResult = layout;
|
||||||
|
}
|
||||||
const result = await d2.compile(JSON.stringify(data));
|
const result = await d2.compile(JSON.stringify(data));
|
||||||
const response = JSON.parse(result);
|
const response = JSON.parse(result);
|
||||||
if (response.error) throw new Error(response.error.message);
|
if (response.error) throw new Error(response.error.message);
|
||||||
|
|
|
||||||
|
|
@ -27,11 +27,9 @@ export function setupMessageHandler(isNode, port, initWasm) {
|
||||||
|
|
||||||
case "compile":
|
case "compile":
|
||||||
try {
|
try {
|
||||||
if (data.options.layout === "elk" || data.options.layout == null) {
|
if (data.options.layout === "elk") {
|
||||||
const elkGraph = await d2.getELKGraph(JSON.stringify(data));
|
const elkGraph = await d2.getELKGraph(JSON.stringify(data));
|
||||||
const response = JSON.parse(elkGraph);
|
const elkGraph2 = JSON.parse(elkGraph).data;
|
||||||
if (response.error) throw new Error(response.error.message);
|
|
||||||
const elkGraph2 = response.data;
|
|
||||||
const layout = await elk.layout(elkGraph2);
|
const layout = await elk.layout(elkGraph2);
|
||||||
globalThis.elkResult = layout;
|
globalThis.elkResult = layout;
|
||||||
}
|
}
|
||||||
|
|
@ -49,10 +47,7 @@ export function setupMessageHandler(isNode, port, initWasm) {
|
||||||
const result = await d2.render(JSON.stringify(data));
|
const result = await d2.render(JSON.stringify(data));
|
||||||
const response = JSON.parse(result);
|
const response = JSON.parse(result);
|
||||||
if (response.error) throw new Error(response.error.message);
|
if (response.error) throw new Error(response.error.message);
|
||||||
const decoded = new TextDecoder().decode(
|
currentPort.postMessage({ type: "result", data: atob(response.data) });
|
||||||
Uint8Array.from(atob(response.data), (c) => c.charCodeAt(0))
|
|
||||||
);
|
|
||||||
currentPort.postMessage({ type: "result", data: decoded });
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
currentPort.postMessage({ type: "error", error: err.message });
|
currentPort.postMessage({ type: "error", error: err.message });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,29 +16,6 @@ describe("D2 Unit Tests", () => {
|
||||||
await d2.worker.terminate();
|
await d2.worker.terminate();
|
||||||
}, 20000);
|
}, 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 () => {
|
test("render works", async () => {
|
||||||
const d2 = new D2();
|
const d2 = new D2();
|
||||||
const result = await d2.compile("x -> y");
|
const result = await d2.compile("x -> y");
|
||||||
|
|
@ -48,61 +25,15 @@ describe("D2 Unit Tests", () => {
|
||||||
await d2.worker.terminate();
|
await d2.worker.terminate();
|
||||||
}, 20000);
|
}, 20000);
|
||||||
|
|
||||||
test("d2-config read correctly", async () => {
|
test("multiple renders works", async () => {
|
||||||
const d2 = new D2();
|
const d2 = new D2();
|
||||||
const result = await d2.compile(
|
const result = await d2.compile("x -> y");
|
||||||
`
|
const svg = await d2.render(result.diagram);
|
||||||
vars: {
|
expect(svg).toContain("<svg");
|
||||||
d2-config: {
|
expect(svg).toContain("</svg>");
|
||||||
theme-id: 4
|
const result2 = await d2.compile("x -> y");
|
||||||
dark-theme-id: 200
|
const svg2 = await d2.render(result2.diagram);
|
||||||
pad: 10
|
expect(svg).toEqual(svg2);
|
||||||
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();
|
await d2.worker.terminate();
|
||||||
}, 20000);
|
}, 20000);
|
||||||
|
|
||||||
|
|
@ -116,52 +47,6 @@ x -> y
|
||||||
await d2.worker.terminate();
|
await d2.worker.terminate();
|
||||||
}, 20000);
|
}, 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 () => {
|
test("latex works", async () => {
|
||||||
const d2 = new D2();
|
const d2 = new D2();
|
||||||
const result = await d2.compile("x: |latex \\frac{f(x+h)-f(x)}{h} |");
|
const result = await d2.compile("x: |latex \\frac{f(x+h)-f(x)}{h} |");
|
||||||
|
|
@ -171,17 +56,6 @@ layers: {
|
||||||
await d2.worker.terminate();
|
await d2.worker.terminate();
|
||||||
}, 20000);
|
}, 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 () => {
|
test("handles syntax errors correctly", async () => {
|
||||||
const d2 = new D2();
|
const d2 = new D2();
|
||||||
try {
|
try {
|
||||||
|
|
@ -193,42 +67,4 @@ layers: {
|
||||||
}
|
}
|
||||||
await d2.worker.terminate();
|
await d2.worker.terminate();
|
||||||
}, 20000);
|
}, 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);
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -190,20 +190,6 @@ 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) {
|
if math.IsInf(x1, 1) && math.IsInf(x2, -1) {
|
||||||
x1 = 0
|
x1 = 0
|
||||||
x2 = 0
|
x2 = 0
|
||||||
|
|
|
||||||
|
|
@ -77,15 +77,6 @@ func Compile(ctx context.Context, input string, compileOpts *CompileOptions, ren
|
||||||
|
|
||||||
d, err := compile(ctx, g, compileOpts, renderOpts)
|
d, err := compile(ctx, g, compileOpts, renderOpts)
|
||||||
if d != nil {
|
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
|
d.Config = config
|
||||||
}
|
}
|
||||||
return d, g, err
|
return d, g, err
|
||||||
|
|
|
||||||
134
d2oracle/edit.go
|
|
@ -398,7 +398,7 @@ func _set(g *d2graph.Graph, baseAST *d2ast.Map, key string, tag, value *string)
|
||||||
if baseAST != g.AST || imported {
|
if baseAST != g.AST || imported {
|
||||||
writeableRefs := GetWriteableRefs(obj, baseAST)
|
writeableRefs := GetWriteableRefs(obj, baseAST)
|
||||||
for _, ref := range writeableRefs {
|
for _, ref := range writeableRefs {
|
||||||
if ref.MapKey != nil && ref.MapKey.Value.Map != nil && ref.MapKey.Key == mk.Key {
|
if ref.MapKey != nil && ref.MapKey.Value.Map != nil {
|
||||||
maybeNewScope = ref.MapKey.Value.Map
|
maybeNewScope = ref.MapKey.Value.Map
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3324,135 +3324,3 @@ func filterReservedPath(path []*d2ast.StringBox) (filtered []*d2ast.StringBox) {
|
||||||
}
|
}
|
||||||
return
|
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,7 +30,6 @@ func TestCreate(t *testing.T) {
|
||||||
boardPath []string
|
boardPath []string
|
||||||
name string
|
name string
|
||||||
text string
|
text string
|
||||||
fsTexts map[string]string
|
|
||||||
key string
|
key string
|
||||||
|
|
||||||
expKey string
|
expKey string
|
||||||
|
|
@ -806,35 +805,6 @@ steps: {
|
||||||
d 2
|
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)
|
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -847,7 +817,6 @@ a.(b -> c)
|
||||||
var newKey string
|
var newKey string
|
||||||
et := editTest{
|
et := editTest{
|
||||||
text: tc.text,
|
text: tc.text,
|
||||||
fsTexts: tc.fsTexts,
|
|
||||||
testFunc: func(g *d2graph.Graph) (*d2graph.Graph, error) {
|
testFunc: func(g *d2graph.Graph) (*d2graph.Graph, error) {
|
||||||
var err error
|
var err error
|
||||||
g, newKey, err = d2oracle.Create(g, tc.boardPath, tc.key)
|
g, newKey, err = d2oracle.Create(g, tc.boardPath, tc.key)
|
||||||
|
|
@ -2775,40 +2744,6 @@ scenarios: {
|
||||||
Metricbeat.style.stroke: red
|
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"}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -6180,28 +6115,6 @@ y
|
||||||
exp: `y
|
exp: `y
|
||||||
a -> b
|
a -> b
|
||||||
c -> d
|
c -> d
|
||||||
`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "underscore_linked",
|
|
||||||
text: `k
|
|
||||||
|
|
||||||
layers: {
|
|
||||||
x: {
|
|
||||||
a
|
|
||||||
b: {link: _}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
key: `b`,
|
|
||||||
boardPath: []string{"x"},
|
|
||||||
exp: `k
|
|
||||||
|
|
||||||
layers: {
|
|
||||||
x: {
|
|
||||||
a
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -8164,32 +8077,6 @@ y
|
||||||
y
|
y
|
||||||
|
|
||||||
(* -> *)[*].style.opacity: 0.8
|
(* -> *)[*].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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -9597,288 +9484,3 @@ 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,37 +48,53 @@ func ReplaceBoardNode(ast, ast2 *d2ast.Map, boardPath []string) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return replaceBoardNodeInMap(ast, ast2, boardPath, "layers") ||
|
findMap := func(root *d2ast.Map, name string) *d2ast.Map {
|
||||||
replaceBoardNodeInMap(ast, ast2, boardPath, "scenarios") ||
|
for _, n := range root.Nodes {
|
||||||
replaceBoardNodeInMap(ast, ast2, boardPath, "steps")
|
if n.MapKey != nil && n.MapKey.Key != nil && n.MapKey.Key.Path[0].Unbox().ScalarString() == name {
|
||||||
}
|
return n.MapKey.Value.Map
|
||||||
|
|
||||||
func replaceBoardNodeInMap(ast, ast2 *d2ast.Map, boardPath []string, boardType string) bool {
|
|
||||||
var matches []*d2ast.Map
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
for _, boardMap := range matches {
|
layersMap := findMap(ast, "layers")
|
||||||
for _, n := range boardMap.Nodes {
|
scenariosMap := findMap(ast, "scenarios")
|
||||||
if n.MapKey != nil && n.MapKey.Key != nil &&
|
stepsMap := findMap(ast, "steps")
|
||||||
n.MapKey.Key.Path[0].Unbox().ScalarString() == boardPath[0] &&
|
|
||||||
n.MapKey.Value.Map != nil {
|
if layersMap != nil {
|
||||||
|
m := findMap(layersMap, boardPath[0])
|
||||||
|
if m != nil {
|
||||||
if len(boardPath) > 1 {
|
if len(boardPath) > 1 {
|
||||||
if ReplaceBoardNode(n.MapKey.Value.Map, ast2, boardPath[1:]) {
|
return ReplaceBoardNode(m, ast2, boardPath[1:])
|
||||||
return true
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
n.MapKey.Value.Map.Nodes = ast2.Nodes
|
m.Nodes = ast2.Nodes
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
|
|
|
||||||
|
|
@ -984,14 +984,6 @@ func (p *parser) parseKey() (k *d2ast.KeyPath) {
|
||||||
k = nil
|
k = nil
|
||||||
} else {
|
} else {
|
||||||
k.Range.End = k.Path[len(k.Path)-1].Unbox().GetRange().End
|
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|
@ -1488,6 +1480,12 @@ func (p *parser) parseBlockString() *d2ast.BlockString {
|
||||||
}
|
}
|
||||||
|
|
||||||
if r != endHint {
|
if r != endHint {
|
||||||
|
if (bs.Tag == "latex" || bs.Tag == "tex") && r == '\\' {
|
||||||
|
// For LaTeX, where single backslash is common, we escape it so that users don't have to write double the backslashes
|
||||||
|
sb.WriteRune('\\')
|
||||||
|
sb.WriteRune('\\')
|
||||||
|
continue
|
||||||
|
}
|
||||||
sb.WriteRune(r)
|
sb.WriteRune(r)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
@ -1676,20 +1674,6 @@ func (p *parser) parseValue() d2ast.ValueBox {
|
||||||
}
|
}
|
||||||
return box
|
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") {
|
if strings.EqualFold(s.ScalarString(), "true") {
|
||||||
box.Boolean = &d2ast.Boolean{
|
box.Boolean = &d2ast.Boolean{
|
||||||
|
|
|
||||||
|
|
@ -500,15 +500,6 @@ 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")
|
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)
|
runa(t, tca)
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"oss.terrastruct.com/util-go/xexec"
|
"oss.terrastruct.com/util-go/xexec"
|
||||||
"oss.terrastruct.com/util-go/xmain"
|
"oss.terrastruct.com/util-go/xmain"
|
||||||
|
|
@ -171,7 +170,7 @@ func FindPlugin(ctx context.Context, ps []Plugin, name string) (Plugin, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if strings.EqualFold(info.Name, name) {
|
if info.Name == name {
|
||||||
return p, nil
|
return p, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ func Wrap(rootDiagram *d2target.Diagram, svgs [][]byte, renderOpts d2svg.RenderO
|
||||||
)
|
)
|
||||||
fmt.Fprint(buf, fitToScreenWrapperOpening)
|
fmt.Fprint(buf, fitToScreenWrapperOpening)
|
||||||
|
|
||||||
innerOpening := fmt.Sprintf(`<svg class="d2-svg" width="%d" height="%d" viewBox="%d %d %d %d">`,
|
innerOpening := fmt.Sprintf(`<svg id="d2-svg" width="%d" height="%d" viewBox="%d %d %d %d">`,
|
||||||
width, height, left, top, width, height)
|
width, height, left, top, width, height)
|
||||||
fmt.Fprint(buf, innerOpening)
|
fmt.Fprint(buf, innerOpening)
|
||||||
|
|
||||||
|
|
@ -77,7 +77,7 @@ func Wrap(rootDiagram *d2target.Diagram, svgs [][]byte, renderOpts d2svg.RenderO
|
||||||
svgsStr += string(svg) + " "
|
svgsStr += string(svg) + " "
|
||||||
}
|
}
|
||||||
|
|
||||||
diagramHash, err := rootDiagram.HashID(renderOpts.Salt)
|
diagramHash, err := rootDiagram.HashID()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -102,7 +102,7 @@ func Wrap(rootDiagram *d2target.Diagram, svgs [][]byte, renderOpts d2svg.RenderO
|
||||||
}
|
}
|
||||||
|
|
||||||
if renderOpts.Sketch != nil && *renderOpts.Sketch {
|
if renderOpts.Sketch != nil && *renderOpts.Sketch {
|
||||||
d2sketch.DefineFillPatterns(buf, diagramHash)
|
d2sketch.DefineFillPatterns(buf)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Fprint(buf, `<style type="text/css"><![CDATA[`)
|
fmt.Fprint(buf, `<style type="text/css"><![CDATA[`)
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import (
|
||||||
"math"
|
"math"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"oss.terrastruct.com/d2/lib/jsrunner"
|
"oss.terrastruct.com/d2/lib/jsrunner"
|
||||||
"oss.terrastruct.com/util-go/xdefer"
|
"oss.terrastruct.com/util-go/xdefer"
|
||||||
|
|
@ -29,7 +28,6 @@ var svgRe = regexp.MustCompile(`<svg[^>]+width="([0-9\.]+)ex" height="([0-9\.]+)
|
||||||
|
|
||||||
func Render(s string) (_ string, err error) {
|
func Render(s string) (_ string, err error) {
|
||||||
defer xdefer.Errorf(&err, "latex failed to parse")
|
defer xdefer.Errorf(&err, "latex failed to parse")
|
||||||
s = doubleBackslashes(s)
|
|
||||||
runner := jsrunner.NewJSRunner()
|
runner := jsrunner.NewJSRunner()
|
||||||
|
|
||||||
if _, err := runner.RunString(polyfillsJS); err != nil {
|
if _, err := runner.RunString(polyfillsJS); err != nil {
|
||||||
|
|
@ -84,15 +82,3 @@ func Measure(s string) (width, height int, err error) {
|
||||||
|
|
||||||
return int(math.Ceil(wf * float64(pxPerEx))), int(math.Ceil(hf * float64(pxPerEx))), nil
|
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) {
|
func TestRender(t *testing.T) {
|
||||||
txts := []string{
|
txts := []string{
|
||||||
`a + b = c`,
|
`a + b = c`,
|
||||||
`\frac{1}{2}`,
|
`\\frac{1}{2}`,
|
||||||
`a + b
|
`a + b
|
||||||
= c
|
= c
|
||||||
`,
|
`,
|
||||||
|
|
@ -24,3 +24,10 @@ 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,7 +2,7 @@ const adaptor = MathJax._.adaptors.liteAdaptor.liteAdaptor();
|
||||||
MathJax._.handlers.html_ts.RegisterHTMLHandler(adaptor)
|
MathJax._.handlers.html_ts.RegisterHTMLHandler(adaptor)
|
||||||
const html = MathJax._.mathjax.mathjax.document('', {
|
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'] }),
|
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({fontCache: 'none'}),
|
OutputJax: new MathJax._.output.svg_ts.SVG(),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (typeof globalThis !== 'undefined') {
|
if (typeof globalThis !== 'undefined') {
|
||||||
|
|
|
||||||
|
|
@ -54,22 +54,22 @@ func LoadJS(runner jsrunner.JSRunner) error {
|
||||||
// DefineFillPatterns adds reusable patterns that are overlayed on shapes with
|
// DefineFillPatterns adds reusable patterns that are overlayed on shapes with
|
||||||
// fill. This gives it a subtle streaky effect that subtly looks hand-drawn but
|
// fill. This gives it a subtle streaky effect that subtly looks hand-drawn but
|
||||||
// not distractingly so.
|
// not distractingly so.
|
||||||
func DefineFillPatterns(buf *bytes.Buffer, diagramHash string) {
|
func DefineFillPatterns(buf *bytes.Buffer) {
|
||||||
source := buf.String()
|
source := buf.String()
|
||||||
fmt.Fprint(buf, "<defs>")
|
fmt.Fprint(buf, "<defs>")
|
||||||
|
|
||||||
defineFillPattern(buf, source, diagramHash, "bright", "rgba(0, 0, 0, 0.1)")
|
defineFillPattern(buf, source, "bright", "rgba(0, 0, 0, 0.1)")
|
||||||
defineFillPattern(buf, source, diagramHash, "normal", "rgba(0, 0, 0, 0.16)")
|
defineFillPattern(buf, source, "normal", "rgba(0, 0, 0, 0.16)")
|
||||||
defineFillPattern(buf, source, diagramHash, "dark", "rgba(0, 0, 0, 0.32)")
|
defineFillPattern(buf, source, "dark", "rgba(0, 0, 0, 0.32)")
|
||||||
defineFillPattern(buf, source, diagramHash, "darker", "rgba(255, 255, 255, 0.24)")
|
defineFillPattern(buf, source, "darker", "rgba(255, 255, 255, 0.24)")
|
||||||
|
|
||||||
fmt.Fprint(buf, "</defs>")
|
fmt.Fprint(buf, "</defs>")
|
||||||
}
|
}
|
||||||
|
|
||||||
func defineFillPattern(buf *bytes.Buffer, source, diagramHash string, luminanceCategory, fill string) {
|
func defineFillPattern(buf *bytes.Buffer, source string, luminanceCategory, fill string) {
|
||||||
trigger := fmt.Sprintf(`url(#streaks-%s-%s)`, luminanceCategory, diagramHash)
|
trigger := fmt.Sprintf(`url(#streaks-%s)`, luminanceCategory)
|
||||||
if strings.Contains(source, trigger) {
|
if strings.Contains(source, trigger) {
|
||||||
fmt.Fprintf(buf, streaks, luminanceCategory, diagramHash, fill)
|
fmt.Fprintf(buf, streaks, luminanceCategory, fill)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -788,13 +788,6 @@ func ArrowheadJS(r jsrunner.JSRunner, arrowhead d2target.Arrowhead, stroke strin
|
||||||
stroke,
|
stroke,
|
||||||
stroke,
|
stroke,
|
||||||
)
|
)
|
||||||
case d2target.CrossArrowhead:
|
|
||||||
arrowJS = fmt.Sprintf(
|
|
||||||
`node = rc.linearPath(%s, { strokeWidth: %d, stroke: "%s", seed: 3 })`,
|
|
||||||
`[[-6, -6], [6, 6], [0, 0], [-6, 6], [0, 0], [6, -6]]`,
|
|
||||||
strokeWidth,
|
|
||||||
stroke,
|
|
||||||
)
|
|
||||||
case d2target.CfManyRequired:
|
case d2target.CfManyRequired:
|
||||||
arrowJS = fmt.Sprintf(
|
arrowJS = fmt.Sprintf(
|
||||||
// TODO why does fillStyle: "zigzag" error with path
|
// TODO why does fillStyle: "zigzag" error with path
|
||||||
|
|
@ -847,22 +840,6 @@ func ArrowheadJS(r jsrunner.JSRunner, arrowhead d2target.Arrowhead, stroke strin
|
||||||
stroke,
|
stroke,
|
||||||
BG_COLOR,
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -462,20 +462,6 @@ a.9 <-> b.9: cf-one-required {
|
||||||
source-arrowhead.shape: cf-one-required
|
source-arrowhead.shape: cf-one-required
|
||||||
target-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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -1357,14 +1343,6 @@ item -> customer: is(Adult)
|
||||||
customer -> item: true
|
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)
|
runa(t, tcs)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 130 KiB After Width: | Height: | Size: 129 KiB |
|
Before Width: | Height: | Size: 121 KiB After Width: | Height: | Size: 120 KiB |
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 146 KiB After Width: | Height: | Size: 131 KiB |
|
Before Width: | Height: | Size: 123 KiB After Width: | Height: | Size: 122 KiB |
168
d2renderers/d2sketch/testdata/basic/sketch.exp.svg
vendored
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB |
164
d2renderers/d2sketch/testdata/class/sketch.exp.svg
vendored
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 166 KiB After Width: | Height: | Size: 165 KiB |
|
Before Width: | Height: | Size: 157 KiB After Width: | Height: | Size: 156 KiB |
178
d2renderers/d2sketch/testdata/dots-3d/sketch.exp.svg
vendored
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 164 KiB After Width: | Height: | Size: 164 KiB |
|
Before Width: | Height: | Size: 169 KiB After Width: | Height: | Size: 168 KiB |
|
Before Width: | Height: | Size: 115 KiB After Width: | Height: | Size: 115 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 66 KiB |
583
d2renderers/d2sketch/testdata/opacity/sketch.exp.svg
vendored
|
Before Width: | Height: | Size: 110 KiB After Width: | Height: | Size: 110 KiB |
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 101 KiB |
180
d2renderers/d2sketch/testdata/overlay/sketch.exp.svg
vendored
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 498 KiB After Width: | Height: | Size: 497 KiB |
|
Before Width: | Height: | Size: 125 KiB After Width: | Height: | Size: 124 KiB |
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 114 KiB After Width: | Height: | Size: 113 KiB |
|
Before Width: | Height: | Size: 53 KiB |
659
d2renderers/d2sketch/testdata/twitter/sketch.exp.svg
vendored
|
Before Width: | Height: | Size: 218 KiB After Width: | Height: | Size: 218 KiB |
|
Before Width: | Height: | Size: 218 KiB After Width: | Height: | Size: 218 KiB |
|
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 70 KiB |