Compare commits

..

1 commit

Author SHA1 Message Date
Alexander Wang
45e3d41b75
add test 2025-01-20 14:28:02 -08:00
2080 changed files with 88151 additions and 153003 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)
[![ci](https://github.com/terrastruct/d2/actions/workflows/ci.yml/badge.svg)](https://github.com/terrastruct/d2/actions/workflows/ci.yml) [![ci](https://github.com/terrastruct/d2/actions/workflows/ci.yml/badge.svg)](https://github.com/terrastruct/d2/actions/workflows/ci.yml)
[![daily](https://github.com/terrastruct/d2/actions/workflows/daily.yml/badge.svg)](https://github.com/terrastruct/d2/actions/workflows/daily.yml) [![daily](https://github.com/terrastruct/d2/actions/workflows/daily.yml/badge.svg)](https://github.com/terrastruct/d2/actions/workflows/daily.yml)
[![release](https://img.shields.io/github/v/release/terrastruct/d2)](https://github.com/terrastruct/d2/releases) [![release](https://img.shields.io/github/v/release/terrastruct/d2)](https://github.com/terrastruct/d2/releases)
[![changelog](https://img.shields.io/badge/changelog-read-blue)](./CHANGELOG.md)
[![npm version](https://img.shields.io/npm/v/@terrastruct/d2)](https://www.npmjs.com/package/@terrastruct/d2)
[![discord](https://img.shields.io/discord/1039184639652265985?label=discord)](https://discord.gg/NF6X8K4eDq) [![discord](https://img.shields.io/discord/1039184639652265985?label=discord)](https://discord.gg/NF6X8K4eDq)
[![twitter](https://img.shields.io/twitter/follow/terrastruct?style=social)](https://twitter.com/terrastruct) [![twitter](https://img.shields.io/twitter/follow/terrastruct?style=social)](https://twitter.com/terrastruct)
[![license](https://img.shields.io/github/license/terrastruct/d2?color=9cf)](./LICENSE.txt) [![license](https://img.shields.io/github/license/terrastruct/d2?color=9cf)](./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)

View file

@ -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 \

View file

@ -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)

View file

@ -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).

View file

@ -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)

View file

@ -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).

View file

@ -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'

View file

@ -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 "$@"

View file

@ -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

View file

@ -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)
} }

View file

@ -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)
} }

View file

@ -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 {

View file

@ -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())

View file

@ -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())
} }

View file

@ -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 {

View file

@ -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)
}
}

View file

@ -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);

View file

@ -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
}

View file

@ -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, ""
}

View file

@ -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
}
}
}
}

View file

@ -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 {

View file

@ -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
} }

View file

@ -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)

View file

@ -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 {

View file

@ -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})"
}
`, `,
}, },
} }

View file

@ -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 == "" {

View file

@ -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)
}
}
}

View file

@ -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`)
}, },
}, },
{ {

View file

@ -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

View file

@ -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:

View file

@ -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}

View file

@ -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 {

View file

@ -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

View file

@ -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!

View file

@ -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

Binary file not shown.

View file

@ -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);

View file

@ -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
View file

@ -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 */;
}

View file

@ -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",

View file

@ -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) {

View file

@ -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 });
} }

View file

@ -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);

View file

@ -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 });
} }

View file

@ -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);
}); });

View file

@ -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

View file

@ -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

View file

@ -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
}

View file

@ -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)
}
})
}
}

View file

@ -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

View file

@ -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{

View file

@ -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)

View file

@ -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
} }
} }

View file

@ -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[`)

View file

@ -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()
}

View file

@ -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")
}
}

View file

@ -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') {

View file

@ -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
} }

View file

@ -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)
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 130 KiB

After

Width:  |  Height:  |  Size: 129 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 121 KiB

After

Width:  |  Height:  |  Size: 120 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 68 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 59 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 146 KiB

After

Width:  |  Height:  |  Size: 131 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 123 KiB

After

Width:  |  Height:  |  Size: 122 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 64 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 53 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 76 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 81 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 59 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 166 KiB

After

Width:  |  Height:  |  Size: 165 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 157 KiB

After

Width:  |  Height:  |  Size: 156 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 164 KiB

After

Width:  |  Height:  |  Size: 164 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 169 KiB

After

Width:  |  Height:  |  Size: 168 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 115 KiB

After

Width:  |  Height:  |  Size: 115 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 77 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 59 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 66 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 110 KiB

After

Width:  |  Height:  |  Size: 110 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 101 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 57 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 498 KiB

After

Width:  |  Height:  |  Size: 497 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 125 KiB

After

Width:  |  Height:  |  Size: 124 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 77 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 68 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 114 KiB

After

Width:  |  Height:  |  Size: 113 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 53 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 218 KiB

After

Width:  |  Height:  |  Size: 218 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 218 KiB

After

Width:  |  Height:  |  Size: 218 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 70 KiB

Some files were not shown because too many files have changed in this diff Show more