Merge branch 'master' into nested-sequence-diagrams

This commit is contained in:
Júlio César Batista 2022-12-01 14:03:35 -08:00
commit f9f02015e6
No known key found for this signature in database
GPG key ID: 10C4B861BF314878
92 changed files with 1049 additions and 1504 deletions

View file

@ -2,9 +2,7 @@ name: daily
on:
workflow_dispatch:
schedule:
# daily at 00:42 to avoid hourly loads in GitHub actions
# https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#schedule
- cron: '42 0 * * *'
- cron: '42 0 * * *' # daily at 00:42
concurrency:
group: ${{ github.workflow }}
cancel-in-progress: true

15
.github/workflows/project.yml vendored Normal file
View file

@ -0,0 +1,15 @@
name: d2-project
on:
issues:
types:
- opened
jobs:
d2-project:
runs-on: ubuntu-latest
steps:
- uses: actions/add-to-project@v0.4.0
with:
project-url: https://github.com/orgs/terrastruct/projects/34
github-token: ${{ secrets._GITHUB_TOKEN }}

1
.gitignore vendored
View file

@ -4,3 +4,4 @@
*.got.json
*.got.svg
e2e_report.html
bin

View file

@ -5,7 +5,7 @@ all: fmt gen lint build test
.PHONY: fmt
fmt:
prefix "$@" ./ci/sub/fmt/make.sh
prefix "$@" ./ci/fmt.sh
.PHONY: gen
gen:
prefix "$@" ./ci/gen.sh

112
README.md
View file

@ -4,7 +4,7 @@
A modern diagram scripting language that turns text to diagrams.
</h2>
[Language docs](https://d2lang.com) | [Cheat sheet](./docs/assets/cheat_sheet.pdf)
[Language docs](https://d2lang.com) | [Cheat sheet](./docs/assets/cheat_sheet.pdf) | [Comparisons](https://text-to-diagram.com)
[![ci](https://github.com/terrastruct/d2/actions/workflows/ci.yml/badge.svg)](https://github.com/terrastruct/d2/actions/workflows/ci.yml)
[![release](https://img.shields.io/github/v/release/terrastruct/d2)](https://github.com/terrastruct/d2/releases)
@ -19,28 +19,26 @@
# Table of Contents
<!-- toc -->
- <a href="#what-does-d2-look-like" id="toc-what-does-d2-look-like">What does D2 look like?</a>
- <a href="#quickstart" id="toc-quickstart">Quickstart</a>
- <a href="#install" id="toc-install">Install</a>
- <a href="#d2-as-a-library" id="toc-d2-as-a-library">D2 as a library</a>
- <a href="#themes" id="toc-themes">Themes</a>
- <a href="#fonts" id="toc-fonts">Fonts</a>
- <a href="#export-file-types" id="toc-export-file-types">Export file types</a>
- <a href="#language-tooling" id="toc-language-tooling">Language tooling</a>
- <a href="#plugins" id="toc-plugins">Plugins</a>
- <a href="#comparison" id="toc-comparison">Comparison</a>
- <a href="#contributing" id="toc-contributing">Contributing</a>
- <a href="#license" id="toc-license">License</a>
- <a href="#related" id="toc-related">Related</a>
- <a href="#vscode-extension" id="toc-vscode-extension">VSCode extension</a>
- <a href="#vim-extension" id="toc-vim-extension">Vim extension</a>
- <a href="#language-docs" id="toc-language-docs">Language docs</a>
- <a href="#misc" id="toc-misc">Misc</a>
- <a href="#faq" id="toc-faq">FAQ</a>
- [What does D2 look like?](#what-does-d2-look-like)
- [Quickstart](#quickstart)
- [Install](#install)
- [D2 as a library](#d2-as-a-library)
- [Themes](#themes)
- [Fonts](#fonts)
- [Export file types](#export-file-types)
- [Language tooling](#language-tooling)
- [Plugins](#plugins)
- [Comparison](#comparison)
- [Contributing](#contributing)
- [License](#license)
- [Related](#related)
* [VSCode extension](#vscode-extension)
* [Vim extension](#vim-extension)
* [Misc](#misc)
- [FAQ](#faq)
<!-- tocstop -->
# What does D2 look like?
## What does D2 look like?
```d2
# Actors
@ -97,70 +95,34 @@ The easiest way to install is with our install script:
curl -fsSL https://d2lang.com/install.sh | sh -s --
```
To uninstall:
You can run the install script with `--dry-run` to see the commands that will be used
to install without executing them.
Or if you have Go installed you can install from source though you won't get the manpage:
```sh
go install oss.terrastruct.com/d2@latest
```
To uninstall with the install script:
```sh
curl -fsSL https://d2lang.com/install.sh | sh -s -- --uninstall
```
For detailed installation docs, with alternative methods and examples for each OS, see
[./docs/INSTALL.md](./docs/INSTALL.md).
For detailed installation docs, see [./docs/INSTALL.md](./docs/INSTALL.md).
We demonstrate alternative methods and examples for each OS.
As well, the functioning of the install script is described in detail to alleviate any
concern of its use. We recommend using your OS's package manager directly instead for
improved security but the install script is by no means insecure.
## D2 as a library
In addition to being a runnable CLI tool, D2 can also be used to produce diagrams from
Go programs.
```go
import (
"context"
"io/ioutil"
"path/filepath"
"strings"
"oss.terrastruct.com/d2/d2compiler"
"oss.terrastruct.com/d2/d2exporter"
"oss.terrastruct.com/d2/d2layouts/d2dagrelayout"
"oss.terrastruct.com/d2/d2renderers/d2svg"
"oss.terrastruct.com/d2/d2renderers/textmeasure"
"oss.terrastruct.com/d2/d2themes/d2themescatalog"
)
func main() {
graph, _ := d2compiler.Compile("", strings.NewReader("x -> y"), &d2compiler.CompileOptions{UTF16: true})
ruler, _ := textmeasure.NewRuler()
graph.SetDimensions(nil, ruler)
d2dagrelayout.Layout(context.Background(), graph)
diagram, _ := d2exporter.Export(context.Background(), graph, d2themescatalog.NeutralDefault.ID)
out, _ := d2svg.Render(diagram)
ioutil.WriteFile(filepath.Join("out.svg"), out, 0600)
}
```
D2 is built to be hackable -- the language has an API built on top of it to make edits
programmatically. Modifying the above diagram:
```go
import (
"oss.terrastruct.com/d2/d2renderers/textmeasure"
"oss.terrastruct.com/d2/d2themes/d2themescatalog"
)
// Create a shape with the ID, "meow"
graph, _, _ = d2oracle.Create(graph, "meow")
// Style the shape green
color := "green"
graph, _ = d2oracle.Set(graph, "meow.style.fill", nil, &color)
// Create a shape with the ID, "cat"
graph, _, _ = d2oracle.Create(graph, "cat")
// Move the shape "meow" inside the container "cat"
graph, _ = d2oracle.Move(graph, "meow", "cat.meow")
// Prints formatted D2 script
println(d2format.Format(graph.AST))
```
This makes it easy to build functionality on top of D2. Terrastruct uses the above API to
implement editing of D2 from mouse actions in a visual interface.
For examples, see [./docs/examples/lib](./docs/examples/lib).
## Themes

7
ci/dev.sh Executable file
View file

@ -0,0 +1,7 @@
#!/bin/sh
set -eu
cd -- "$(dirname "$0")/.."
. ./ci/sub/lib.sh
sh_c go build --tags=dev -o=bin/d2 .
sh_c ./bin/d2 "$@"

12
ci/fmt.sh Executable file
View file

@ -0,0 +1,12 @@
#!/bin/sh
set -eu
. "$(dirname "$0")/sub/lib.sh"
cd -- "$(dirname "$0")/.."
if is_changed README.md; then
sh_c tocsubst --skip 1 README.md
fi
if is_changed docs/INSTALL.md; then
sh_c tocsubst --skip 1 docs/INSTALL.md
fi
./ci/sub/fmt/make.sh

View file

@ -14,7 +14,7 @@ export GOOS=$(goos "$OS")
export GOARCH="$ARCH"
sh_c mkdir -p "$HW_BUILD_DIR/bin"
sh_c go build -ldflags "'-X oss.terrastruct.com/d2/lib/version.Version=$VERSION'" \
-o "$HW_BUILD_DIR/bin/d2" ./cmd/d2
-o "$HW_BUILD_DIR/bin/d2" .
ARCHIVE=$PWD/$ARCHIVE
cd "$(dirname "$HW_BUILD_DIR")"

View file

@ -91,6 +91,9 @@ note: Deleting the unarchived releases will cause --uninstall to stop working.
You can rerun install.sh to update your version of D2. install.sh will avoid reinstalling
if the installed version is the latest unless --force is passed.
See https://github.com/terrastruct/d2/blob/master/docs/INSTALL.md#security for
documentation on its security.
EOF
}
@ -450,13 +453,10 @@ uninstall_tala_brew() {
}
is_prefix_writable() {
sh_c "mkdir -p '$INSTALL_DIR' 2>/dev/null" || true
# The reason for checking whether $INSTALL_DIR is writable is that on macOS you have
# /usr/local owned by root but you don't need root to write to its subdirectories which
# is all we want to do.
if [ ! -w "$INSTALL_DIR" ]; then
return 1
fi
is_writable_dir "$INSTALL_DIR"
}
cache_dir() {
@ -509,4 +509,7 @@ brew() {
HOMEBREW_NO_INSTALL_CLEANUP=1 HOMEBREW_NO_AUTO_UPDATE=1 command brew "$@"
}
# The main function does more than provide organization. It provides robustness in that if
# the install script was to only partial download into sh, sh will not execute it because
# main is not invoked until the very last byte.
main "$@"

View file

@ -6,6 +6,7 @@ cd -- "$(dirname "$0")/../.."
help() {
cat <<EOF
usage: $0 [--rebuild] [--local] [--dry-run] [--run=regex] [--host-only] [--lockfile-force]
[--install] [--uninstall]
$0 builds D2 release archives into ./ci/release/build/<version>/d2-<VERSION>-<OS>-<ARCH>.tar.gz
@ -38,6 +39,12 @@ Flags:
--lockfile-force
Forcefully take ownership of remote builder lockfiles.
--install
Ensure a release using --host-only and install it.
--uninstall
Ensure a release using --host-only and uninstall it.
EOF
}
@ -77,6 +84,18 @@ main() {
flag_noarg && shift "$FLAGSHIFT"
LOCKFILE_FORCE=1
;;
install)
flag_noarg && shift "$FLAGSHIFT"
INSTALL=1
HOST_ONLY=1
LOCAL=1
;;
uninstall)
flag_noarg && shift "$FLAGSHIFT"
UNINSTALL=1
HOST_ONLY=1
LOCAL=1
;;
*)
flag_errusage "unrecognized flag $FLAGRAW"
;;
@ -90,8 +109,12 @@ main() {
VERSION=${VERSION:-$(git_describe_ref)}
BUILD_DIR=ci/release/build/$VERSION
if [ -n "${HOST_ONLY-}" ]; then
runjob $(os)-$(arch) "OS=$(os) ARCH=$(arch) build" &
waitjobs
runjob $(os)-$(arch) "OS=$(os) ARCH=$(arch) build"
if [ -n "${INSTALL-}" ]; then
( sh_c make -sC "ci/release/build/$VERSION/$(os)-$(arch)/d2-$VERSION" install)
elif [ -n "${UNINSTALL-}" ]; then
( sh_c make -sC "ci/release/build/$VERSION/$(os)-$(arch)/d2-$VERSION" uninstall)
fi
return 0
fi

View file

@ -1,5 +1,7 @@
#### Features 🚀
#### Features 💸
- Formatting of d2 scripts is supported on the CLI with the `fmt` subcommand.
[#292](https://github.com/terrastruct/d2/pull/292)
- Latex is now supported. See [docs](https://d2lang.com/tour/text) for more.
[#229](https://github.com/terrastruct/d2/pull/229)
- `direction` keyword is now supported to specify `up`, `down`, `right`, `left` layouts. See
@ -12,8 +14,10 @@
- Querying shapes and connections by ID is now supported in renders. [#218](https://github.com/terrastruct/d2/pull/218)
- [install.sh](./install.sh) now accepts `-d` as an alias for `--dry-run`.
[#266](https://github.com/terrastruct/d2/pull/266)
- `-b/--bundle` flag to `d2` now works and bundles all image assets directly as base64
data urls. [#278](https://github.com/terrastruct/d2/pull/278)
#### Improvements 🔧
#### Improvements 🧹
- Local images can now be included, e.g. `icon: ./my_img.png`.
[#146](https://github.com/terrastruct/d2/issues/146)
@ -21,8 +25,14 @@
[#251](https://github.com/terrastruct/d2/pull/251)
- [install.sh](./install.sh) prints the dry run message more visibly.
[#266](https://github.com/terrastruct/d2/pull/266)
- `d2` now lives in the root folder of the repository instead of as a subcommand.
So you can run `go install oss.terrastruct.com/d2@latest` to install from source
now.
[#290](https://github.com/terrastruct/d2/pull/290)
- `BROWSER=0` now works to disable opening a browser on `--watch`.
[#311](https://github.com/terrastruct/d2/pull/311)
#### Bugfixes 🔴
#### Bugfixes ⛑️
- 3D style was missing border and other styles for its top and right faces.
[#187](https://github.com/terrastruct/d2/pull/187)
@ -30,5 +40,10 @@
[#159](https://github.com/terrastruct/d2/issues/159)
- Fixes markdown newlines created with a trailing double space or backslash.
[#214](https://github.com/terrastruct/d2/pull/214)
- Fixes images not loading in PNG exports
- Fixes images not loading in PNG exports.
[#224](https://github.com/terrastruct/d2/pull/224)
- Avoid logging benign file watching errors.
[#293](https://github.com/terrastruct/d2/pull/293)
- `$BROWSER` now works to open a custom browser correctly.
For example, to open Firefox on macOS: `BROWSER='open -aFirefox'`
[#311](https://github.com/terrastruct/d2/pull/311)

View file

@ -1,5 +1,5 @@
#### Features 🚀
#### Features 💸
#### Improvements 🔧
#### Improvements 🧹
#### Bugfixes 🔴
#### Bugfixes ⛑️

View file

@ -14,6 +14,7 @@ sh_c cat >./ci/release/template/scripts/lib.sh <<EOF
#
# - ./ci/sub/lib/rand.sh
# - ./ci/sub/lib/log.sh
# - ./ci/sub/lib/release.sh
#
# Generated by ./ci/release/gen_template_lib.sh.
# *************
@ -25,5 +26,6 @@ EOF
sh_c cat \
./ci/sub/lib/rand.sh \
./ci/sub/lib/log.sh \
./ci/sub/lib/release.sh \
\| sed "-e'/^\. /d'" \>\>./ci/release/template/scripts/lib.sh
sh_c chmod -w ./ci/release/template/scripts/lib.sh

View file

@ -1,6 +1,11 @@
.POSIX:
.SILENT:
.PHONY: all
all:
(. ./scripts/lib.sh && echoerr "You must provide a target of install or uninstall for this Makefile")
exit 1
PREFIX = $(DESTDIR)/usr/local
.PHONY: install

View file

@ -9,16 +9,11 @@
.Op Fl -watch Ar false
.Op Fl -theme Em 0
.Ar file.d2
.Op Ar file.svg
|
.Op Ar file.png
.Nm d2
.Op Fl -watch Ar false
.Op Fl -theme Em 0
.Ar file.d2
.Op Ar ...
.Op Ar file.svg | file.png
.Nm d2
.Ar layout Op Ar name
.Nm d2
.Ar fmt Ar file.d2
.Sh DESCRIPTION
.Nm
compiles and renders
@ -29,10 +24,21 @@ to
.Ar file.png
.Ns .
.Pp
It defaults to
.Ar file.svg
if no output path is passed.
.Pp
Pass - to have
.Nm
read from stdin or write to stdout.
.Pp
Never use the presence of the output file to check for success.
Always use the exit status of
.Nm d2
.Ns .
This is because sometimes when errors occur while rendering, d2 still write out a partial
render anyway to enable iteration on a broken diagram.
.Pp
See more docs, the source code and license at
.Lk https://oss.terrastruct.com/d2
.Sh OPTIONS
@ -71,6 +77,10 @@ Print version information and exit.
Lists available layout engine options with short help.
.It Ar layout Op Ar name
Display long help for a particular layout engine.
.It Ar fmt Ar file.d2
Format
.Ar file.d2
.Ns .
.El
.Sh SEE ALSO
.Xr d2plugin-tala 1

View file

@ -9,10 +9,15 @@ main() {
return 1
fi
sh_c mkdir -p "$PREFIX/bin"
sh_c install ./bin/d2 "$PREFIX/bin/d2"
sh_c mkdir -p "$PREFIX/share/man/man1"
sh_c install ./man/d2.1 "$PREFIX/share/man/man1"
sh_c="sh_c"
if ! is_writable_dir "$PREFIX/bin"; then
sh_c="sudo_sh_c"
fi
"$sh_c" mkdir -p "$PREFIX/bin"
"$sh_c" install ./bin/d2 "$PREFIX/bin/d2"
"$sh_c" mkdir -p "$PREFIX/share/man/man1"
"$sh_c" install ./man/d2.1 "$PREFIX/share/man/man1"
}
main "$@"

View file

@ -7,6 +7,7 @@
#
# - ./ci/sub/lib/rand.sh
# - ./ci/sub/lib/log.sh
# - ./ci/sub/lib/release.sh
#
# Generated by ./ci/release/gen_template_lib.sh.
# *************
@ -55,22 +56,26 @@ tput() {
should_color() {
if [ -n "${COLOR-}" ]; then
if [ "$COLOR" = 0 -o "$COLOR" = false ]; then
_COLOR=
return 1
elif [ "$COLOR" = 1 -o "$COLOR" = true ]; then
if [ "$COLOR" = 1 -o "$COLOR" = true ]; then
_COLOR=1
__COLOR=1
return 0
elif [ "$COLOR" = 0 -o "$COLOR" = false ]; then
_COLOR=
__COLOR=0
return 1
else
printf '$COLOR must be 0, 1, false or true but got %s\n' "$COLOR" >&2
fi
fi
if [ -t 1 ]; then
if [ -t 1 -a "${TERM-}" != dumb ]; then
_COLOR=1
__COLOR=1
return 0
else
_COLOR=
__COLOR=0
return 1
fi
}
@ -89,7 +94,8 @@ _echo() {
get_rand_color() {
# 1-6 are regular and 9-14 are bright.
# 1,2 and 9,10 are red and green but we use those for success and failure.
pick "$*" 3 4 5 6 11 12 13 14
pick "$*" 1 2 3 4 5 6 \
9 10 11 12 13 14
}
echop() {
@ -113,9 +119,9 @@ printfp() {(
fi
should_color || true
if [ $# -eq 0 ]; then
printf '%s' "$(COLOR=${_COLOR-} setaf "$FGCOLOR" "$prefix")"
printf '%s' "$(COLOR=$__COLOR setaf "$FGCOLOR" "$prefix")"
else
printf '%s: %s\n' "$(COLOR=${_COLOR-} setaf "$FGCOLOR" "$prefix")" "$(printf "$@")"
printf '%s: %s\n' "$(COLOR=$__COLOR setaf "$FGCOLOR" "$prefix")" "$(printf "$@")"
fi
)}
@ -124,7 +130,7 @@ catp() {
shift
should_color || true
sed "s/^/$(COLOR=${_COLOR-} printfp "$prefix" '')/"
sed "s/^/$(COLOR=$__COLOR printfp "$prefix" '')/"
}
repeat() {
@ -151,17 +157,17 @@ printferr() {
logp() {
should_color >&2 || true
COLOR=${_COLOR-} echop "$@" | humanpath >&2
COLOR=$__COLOR echop "$@" | humanpath >&2
}
logfp() {
should_color >&2 || true
COLOR=${_COLOR-} printfp "$@" | humanpath >&2
COLOR=$__COLOR printfp "$@" | humanpath >&2
}
logpcat() {
should_color >&2 || true
COLOR=${_COLOR-} catp "$@" | humanpath >&2
COLOR=$__COLOR catp "$@" | humanpath >&2
}
log() {
@ -286,3 +292,63 @@ runtty() {
return 1
esac
}
capcode() {
set +e
"$@"
code=$?
set -e
}
#!/bin/sh
if [ "${LIB_RELEASE-}" ]; then
return 0
fi
LIB_RELEASE=1
goos() {
case $1 in
macos) echo darwin ;;
*) echo $1 ;;
esac
}
os() {
uname=$(uname)
case $uname in
Linux) echo linux ;;
Darwin) echo macos ;;
FreeBSD) echo freebsd ;;
*) echo "$uname" ;;
esac
}
arch() {
uname_m=$(uname -m)
case $uname_m in
aarch64) echo arm64 ;;
x86_64) echo amd64 ;;
*) echo "$uname_m" ;;
esac
}
gh_repo() {
gh repo view --json nameWithOwner --template '{{ .nameWithOwner }}'
}
manpath() {
if command -v manpath >/dev/null; then
command manpath
elif man -w 2>/dev/null; then
man -w
else
echo "${MANPATH-}"
fi
}
is_writable_dir() {
# The path has to exist for -w to succeed.
sh_c "mkdir -p '$1' 2>/dev/null" || true
if [ ! -w "$1" ]; then
return 1
fi
}

View file

@ -9,8 +9,13 @@ main() {
return 1
fi
sh_c rm -f "$PREFIX/bin/d2"
sh_c rm -f "$PREFIX/share/man/man1/d2.1"
sh_c="sh_c"
if ! is_writable_dir "$PREFIX/bin"; then
sh_c="sudo_sh_c"
fi
"$sh_c" rm -f "$PREFIX/bin/d2"
"$sh_c" rm -f "$PREFIX/share/man/man1/d2.1"
}
main "$@"

2
ci/sub

@ -1 +1 @@
Subproject commit 28fb67e3bf11d7df2be9ad57b67b78a1733a7f2d
Subproject commit 70a9ad95ea0ae1de83fa3b7f7d4a160db4853c20

View file

@ -3,8 +3,9 @@
package main
import (
"oss.terrastruct.com/util-go/xmain"
"oss.terrastruct.com/d2/d2plugin"
"oss.terrastruct.com/d2/lib/xmain"
)
func main() {

View file

@ -32,7 +32,7 @@ import (
"unicode/utf16"
"unicode/utf8"
"oss.terrastruct.com/xdefer"
"oss.terrastruct.com/util-go/xdefer"
)
// Node is the base interface implemented by all d2 AST nodes.

View file

@ -9,14 +9,16 @@ import (
"strings"
"testing"
"oss.terrastruct.com/xrand"
"oss.terrastruct.com/util-go/assert"
"oss.terrastruct.com/util-go/xrand"
"oss.terrastruct.com/diff"
"oss.terrastruct.com/util-go/diff"
"oss.terrastruct.com/util-go/go2"
"oss.terrastruct.com/d2/d2ast"
"oss.terrastruct.com/d2/d2format"
"oss.terrastruct.com/d2/d2parser"
"oss.terrastruct.com/d2/lib/go2"
)
func TestRange(t *testing.T) {
@ -193,18 +195,18 @@ func TestRange(t *testing.T) {
var p d2ast.Position
p = p.Advance('a', false)
diff.AssertJSONEq(t, `"0:1:1"`, p)
assert.StringJSON(t, `"0:1:1"`, p)
p = p.Advance('\n', false)
diff.AssertJSONEq(t, `"1:0:2"`, p)
assert.StringJSON(t, `"1:0:2"`, p)
p = p.Advance('è', false)
diff.AssertJSONEq(t, `"1:2:4"`, p)
assert.StringJSON(t, `"1:2:4"`, p)
p = p.Advance('𐀀', false)
diff.AssertJSONEq(t, `"1:6:8"`, p)
assert.StringJSON(t, `"1:6:8"`, p)
p = p.Subtract('𐀀', false)
diff.AssertJSONEq(t, `"1:2:4"`, p)
assert.StringJSON(t, `"1:2:4"`, p)
p = p.Subtract('è', false)
diff.AssertJSONEq(t, `"1:0:2"`, p)
assert.StringJSON(t, `"1:0:2"`, p)
})
t.Run("UTF-16", func(t *testing.T) {
@ -212,18 +214,18 @@ func TestRange(t *testing.T) {
var p d2ast.Position
p = p.Advance('a', true)
diff.AssertJSONEq(t, `"0:1:1"`, p)
assert.StringJSON(t, `"0:1:1"`, p)
p = p.Advance('\n', true)
diff.AssertJSONEq(t, `"1:0:2"`, p)
assert.StringJSON(t, `"1:0:2"`, p)
p = p.Advance('è', true)
diff.AssertJSONEq(t, `"1:1:3"`, p)
assert.StringJSON(t, `"1:1:3"`, p)
p = p.Advance('𐀀', true)
diff.AssertJSONEq(t, `"1:3:5"`, p)
assert.StringJSON(t, `"1:3:5"`, p)
p = p.Subtract('𐀀', true)
diff.AssertJSONEq(t, `"1:1:3"`, p)
assert.StringJSON(t, `"1:1:3"`, p)
p = p.Subtract('è', true)
diff.AssertJSONEq(t, `"1:0:2"`, p)
assert.StringJSON(t, `"1:0:2"`, p)
})
})
}
@ -411,7 +413,7 @@ name to "America".
},
}
diff.AssertJSONEq(t, `{
assert.StringJSON(t, `{
"range": "json_test.d2,0:0:0-5:1:50",
"nodes": [
{
@ -807,7 +809,7 @@ _park `,
t.Parallel()
ast := d2ast.RawString(tc.str, tc.inKey)
diff.AssertStringEq(t, tc.exp, d2format.Format(ast))
assert.String(t, tc.exp, d2format.Format(ast))
})
}
}

View file

@ -6,12 +6,13 @@ import (
"strings"
"time"
"oss.terrastruct.com/util-go/go2"
"oss.terrastruct.com/d2/d2ast"
"oss.terrastruct.com/d2/d2format"
"oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/d2oracle"
"oss.terrastruct.com/d2/d2target"
"oss.terrastruct.com/d2/lib/go2"
)
func GenDSL(maxi int) (_ string, err error) {

View file

@ -17,8 +17,8 @@ import (
"oss.terrastruct.com/d2/d2compiler"
"oss.terrastruct.com/d2/d2exporter"
"oss.terrastruct.com/d2/d2layouts/d2dagrelayout"
"oss.terrastruct.com/d2/d2renderers/textmeasure"
"oss.terrastruct.com/d2/lib/log"
"oss.terrastruct.com/d2/lib/textmeasure"
)
// usage: D2_CHAOS_MAXI=100 D2_CHAOS_N=100 ./ci/test.sh ./d2chaos

View file

@ -8,12 +8,13 @@ import (
"strconv"
"strings"
"oss.terrastruct.com/util-go/go2"
"oss.terrastruct.com/d2/d2ast"
"oss.terrastruct.com/d2/d2format"
"oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/d2parser"
"oss.terrastruct.com/d2/d2target"
"oss.terrastruct.com/d2/lib/go2"
)
// TODO: should Parse even be exported? guess not. IR should contain list of files and

View file

@ -6,9 +6,8 @@ import (
"strings"
"testing"
"oss.terrastruct.com/diff"
"github.com/stretchr/testify/assert"
"oss.terrastruct.com/util-go/assert"
"oss.terrastruct.com/util-go/diff"
"oss.terrastruct.com/d2/d2compiler"
"oss.terrastruct.com/d2/d2format"
@ -775,8 +774,8 @@ x -> y: {
if len(g.Objects) != 2 {
t.Fatalf("expected 2 objects: %#v", g.Objects)
}
diff.AssertStringEq(t, "diamond", g.Edges[0].SrcArrowhead.Shape.Value)
assert.Empty(t, g.Edges[0].Attributes.Shape.Value)
assert.String(t, "diamond", g.Edges[0].SrcArrowhead.Shape.Value)
assert.String(t, "", g.Edges[0].Attributes.Shape.Value)
// Make sure the DSL didn't change. this is a regression test where it did
exp := `x -> y: {
source-arrowhead: {
@ -814,13 +813,13 @@ x -> y: {
if len(g.Objects) != 2 {
t.Fatalf("expected 2 objects: %#v", g.Objects)
}
diff.AssertStringEq(t, "diamond", g.Edges[0].SrcArrowhead.Shape.Value)
diff.AssertStringEq(t, "Reisner's Rule of Conceptual Inertia", g.Edges[0].SrcArrowhead.Label.Value)
diff.AssertStringEq(t, "QOTD", g.Edges[0].DstArrowhead.Label.Value)
diff.AssertStringEq(t, "true", g.Edges[0].DstArrowhead.Style.Filled.Value)
assert.Empty(t, g.Edges[0].Attributes.Shape.Value)
assert.Empty(t, g.Edges[0].Attributes.Label.Value)
assert.Nil(t, g.Edges[0].Attributes.Style.Filled)
assert.String(t, "diamond", g.Edges[0].SrcArrowhead.Shape.Value)
assert.String(t, "Reisner's Rule of Conceptual Inertia", g.Edges[0].SrcArrowhead.Label.Value)
assert.String(t, "QOTD", g.Edges[0].DstArrowhead.Label.Value)
assert.String(t, "true", g.Edges[0].DstArrowhead.Style.Filled.Value)
assert.String(t, "", g.Edges[0].Attributes.Shape.Value)
assert.String(t, "", g.Edges[0].Attributes.Label.Value)
assert.JSON(t, nil, g.Edges[0].Attributes.Style.Filled)
},
},
{
@ -836,8 +835,8 @@ x -> y: {
if len(g.Objects) != 2 {
t.Fatalf("expected 2 objects: %#v", g.Objects)
}
diff.AssertStringEq(t, "diamond", g.Edges[0].SrcArrowhead.Shape.Value)
assert.Empty(t, g.Edges[0].Attributes.Shape.Value)
assert.String(t, "diamond", g.Edges[0].SrcArrowhead.Shape.Value)
assert.String(t, "", g.Edges[0].Attributes.Shape.Value)
},
},
{
@ -853,8 +852,8 @@ x -> y: {
if len(g.Objects) != 2 {
t.Fatalf("expected 2 objects: %#v", g.Objects)
}
diff.AssertStringEq(t, "triangle", g.Edges[0].SrcArrowhead.Shape.Value)
assert.Empty(t, g.Edges[0].Attributes.Shape.Value)
assert.String(t, "triangle", g.Edges[0].SrcArrowhead.Shape.Value)
assert.String(t, "", g.Edges[0].Attributes.Shape.Value)
},
},
{
@ -880,8 +879,8 @@ x -> y: {
if len(g.Objects) != 2 {
t.Fatalf("expected 2 objects: %#v", g.Objects)
}
diff.AssertStringEq(t, "yo", g.Edges[0].SrcArrowhead.Label.Value)
assert.Empty(t, g.Edges[0].Attributes.Label.Value)
assert.String(t, "yo", g.Edges[0].SrcArrowhead.Label.Value)
assert.String(t, "", g.Edges[0].Attributes.Label.Value)
},
},
{
@ -899,8 +898,8 @@ x -> y: {
if len(g.Objects) != 2 {
t.Fatalf("expected 2 objects: %#v", g.Objects)
}
diff.AssertStringEq(t, "diamond", g.Edges[0].SrcArrowhead.Shape.Value)
assert.Empty(t, g.Edges[0].Attributes.Shape.Value)
assert.String(t, "diamond", g.Edges[0].SrcArrowhead.Shape.Value)
assert.String(t, "", g.Edges[0].Attributes.Shape.Value)
},
},
{
@ -920,9 +919,9 @@ x -> y: {
if len(g.Objects) != 2 {
t.Fatalf("expected 2 objects: %#v", g.Objects)
}
diff.AssertStringEq(t, "diamond", g.Edges[0].SrcArrowhead.Shape.Value)
diff.AssertStringEq(t, "diamond", g.Edges[0].DstArrowhead.Shape.Value)
assert.Empty(t, g.Edges[0].Attributes.Shape.Value)
assert.String(t, "diamond", g.Edges[0].SrcArrowhead.Shape.Value)
assert.String(t, "diamond", g.Edges[0].DstArrowhead.Shape.Value)
assert.String(t, "", g.Edges[0].Attributes.Shape.Value)
},
},
{
@ -1333,7 +1332,7 @@ y -> x.style
if len(g.Objects) != 1 {
t.Fatal(g.Objects)
}
diff.AssertStringEq(t, `b
assert.String(t, `b
b`, g.Objects[0].Attributes.Label.Value)
},
},
@ -1420,9 +1419,9 @@ b`, g.Objects[0].Attributes.Label.Value)
if len(g.Objects) != 1 {
t.Fatal(g.Objects)
}
diff.AssertStringEq(t, `field here`, g.Objects[0].Class.Fields[0].Name)
diff.AssertStringEq(t, `GetType()`, g.Objects[0].Class.Methods[0].Name)
diff.AssertStringEq(t, `Is()`, g.Objects[0].Class.Methods[1].Name)
assert.String(t, `field here`, g.Objects[0].Class.Fields[0].Name)
assert.String(t, `GetType()`, g.Objects[0].Class.Methods[0].Name)
assert.String(t, `Is()`, g.Objects[0].Class.Methods[1].Name)
},
},
{
@ -1438,8 +1437,8 @@ b`, g.Objects[0].Attributes.Label.Value)
if len(g.Objects) != 1 {
t.Fatal(g.Objects)
}
diff.AssertStringEq(t, `GetType()`, g.Objects[0].SQLTable.Columns[0].Name)
diff.AssertStringEq(t, `Is()`, g.Objects[0].SQLTable.Columns[1].Name)
assert.String(t, `GetType()`, g.Objects[0].SQLTable.Columns[0].Name)
assert.String(t, `Is()`, g.Objects[0].SQLTable.Columns[1].Name)
},
},
{
@ -1463,8 +1462,8 @@ b`, g.Objects[0].Attributes.Label.Value)
if len(g.Objects[0].ChildrenArray) != 1 {
t.Fatal(g.Objects)
}
diff.AssertStringEq(t, `GetType()`, g.Objects[1].SQLTable.Columns[0].Name)
diff.AssertStringEq(t, `Is()`, g.Objects[1].SQLTable.Columns[1].Name)
assert.String(t, `GetType()`, g.Objects[1].SQLTable.Columns[0].Name)
assert.String(t, `Is()`, g.Objects[1].SQLTable.Columns[1].Name)
},
},
{
@ -1509,7 +1508,7 @@ dst.id <-> src.dst_id
}
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
diff.AssertStringEq(t, "sequence_diagram", g.Objects[0].Attributes.Shape.Value)
assert.String(t, "sequence_diagram", g.Objects[0].Attributes.Shape.Value)
},
},
{
@ -1518,7 +1517,7 @@ dst.id <-> src.dst_id
text: `shape: sequence_diagram
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
diff.AssertStringEq(t, "sequence_diagram", g.Root.Attributes.Shape.Value)
assert.String(t, "sequence_diagram", g.Root.Attributes.Shape.Value)
},
},
{
@ -1526,7 +1525,7 @@ dst.id <-> src.dst_id
text: `direction: right`,
assertions: func(t *testing.T, g *d2graph.Graph) {
diff.AssertStringEq(t, "right", g.Root.Attributes.Direction.Value)
assert.String(t, "right", g.Root.Attributes.Direction.Value)
},
},
{
@ -1534,7 +1533,7 @@ dst.id <-> src.dst_id
text: `x`,
assertions: func(t *testing.T, g *d2graph.Graph) {
diff.AssertStringEq(t, "", g.Objects[0].Attributes.Direction.Value)
assert.String(t, "", g.Objects[0].Attributes.Direction.Value)
},
},
{
@ -1544,7 +1543,7 @@ dst.id <-> src.dst_id
direction: left
}`,
assertions: func(t *testing.T, g *d2graph.Graph) {
diff.AssertStringEq(t, "left", g.Objects[0].Attributes.Direction.Value)
assert.String(t, "left", g.Objects[0].Attributes.Direction.Value)
},
},
{
@ -1594,10 +1593,8 @@ dst.id <-> src.dst_id
Err: err,
}
err = diff.Testdata(filepath.Join("..", "testdata", "d2compiler", t.Name()), got)
if err != nil {
t.Fatal(err)
}
err = diff.TestdataJSON(filepath.Join("..", "testdata", "d2compiler", t.Name()), got)
assert.Success(t, err)
})
}
}

View file

@ -8,18 +8,17 @@ import (
"cdr.dev/slog"
"oss.terrastruct.com/diff"
"github.com/stretchr/testify/assert"
"oss.terrastruct.com/util-go/assert"
"oss.terrastruct.com/util-go/diff"
"oss.terrastruct.com/d2/d2compiler"
"oss.terrastruct.com/d2/d2exporter"
"oss.terrastruct.com/d2/d2layouts/d2dagrelayout"
"oss.terrastruct.com/d2/d2renderers/textmeasure"
"oss.terrastruct.com/d2/d2target"
"oss.terrastruct.com/d2/d2themes/d2themescatalog"
"oss.terrastruct.com/d2/lib/geo"
"oss.terrastruct.com/d2/lib/log"
"oss.terrastruct.com/d2/lib/textmeasure"
)
type testCase struct {
@ -215,10 +214,10 @@ func run(t *testing.T, tc testCase) {
}
ruler, err := textmeasure.NewRuler()
assert.Nil(t, err)
assert.JSON(t, nil, err)
err = g.SetDimensions(nil, ruler)
assert.Nil(t, err)
assert.JSON(t, nil, err)
err = d2dagrelayout.Layout(ctx, g)
if err != nil {
@ -252,8 +251,6 @@ func run(t *testing.T, tc testCase) {
got.Connections[i].LabelPosition = ""
}
err = diff.Testdata(filepath.Join("..", "testdata", "d2exporter", t.Name()), got)
if err != nil {
t.Fatal(err)
}
err = diff.TestdataJSON(filepath.Join("..", "testdata", "d2exporter", t.Name()), got)
assert.Success(t, err)
}

View file

@ -3,7 +3,7 @@ package d2format_test
import (
"testing"
"oss.terrastruct.com/diff"
"oss.terrastruct.com/util-go/assert"
"oss.terrastruct.com/d2/d2ast"
"oss.terrastruct.com/d2/d2format"
@ -42,7 +42,7 @@ func TestEscapeSingleQuoted(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
diff.AssertStringEq(t, tc.exp, d2format.Format(&d2ast.SingleQuotedString{
assert.String(t, tc.exp, d2format.Format(&d2ast.SingleQuotedString{
Value: tc.str,
}))
})
@ -104,7 +104,7 @@ func TestEscapeDoubleQuoted(t *testing.T) {
} else {
n = d2ast.FlatDoubleQuotedString(tc.str)
}
diff.AssertStringEq(t, tc.exp, d2format.Format(n))
assert.String(t, tc.exp, d2format.Format(n))
})
}
}
@ -203,7 +203,7 @@ func TestEscapeUnquoted(t *testing.T) {
n = d2ast.FlatUnquotedString(tc.str)
}
diff.AssertStringEq(t, tc.exp, d2format.Format(n))
assert.String(t, tc.exp, d2format.Format(n))
})
}
}
@ -286,7 +286,7 @@ func TestEscapeBlockString(t *testing.T) {
Value: tc.value,
}
diff.AssertStringEq(t, tc.exp, d2format.Format(n))
assert.String(t, tc.exp, d2format.Format(n))
})
}
}

View file

@ -5,7 +5,7 @@ import (
"strings"
"testing"
"oss.terrastruct.com/diff"
"oss.terrastruct.com/util-go/assert"
"oss.terrastruct.com/d2/d2format"
"oss.terrastruct.com/d2/d2parser"
@ -605,7 +605,7 @@ hi # Fraud is the homage that force pays to reason.
if err != nil {
t.Fatal(err)
}
diff.AssertStringEq(t, tc.exp, d2format.Format(ast))
assert.String(t, tc.exp, d2format.Format(ast))
})
}
}
@ -621,6 +621,6 @@ func TestEdge(t *testing.T) {
t.Fatalf("expected one edge: %#v", mk.Edges)
}
diff.AssertStringEq(t, `x -> y`, d2format.Format(mk.Edges[0]))
diff.AssertStringEq(t, `[0]`, d2format.Format(mk.EdgeIndex))
assert.String(t, `x -> y`, d2format.Format(mk.Edges[0]))
assert.String(t, `[0]`, d2format.Format(mk.EdgeIndex))
}

View file

@ -7,16 +7,17 @@ import (
"strconv"
"strings"
"oss.terrastruct.com/util-go/go2"
"oss.terrastruct.com/d2/d2ast"
"oss.terrastruct.com/d2/d2format"
"oss.terrastruct.com/d2/d2parser"
"oss.terrastruct.com/d2/d2renderers/d2fonts"
"oss.terrastruct.com/d2/d2renderers/d2latex"
"oss.terrastruct.com/d2/d2renderers/textmeasure"
"oss.terrastruct.com/d2/d2target"
"oss.terrastruct.com/d2/d2themes"
"oss.terrastruct.com/d2/lib/geo"
"oss.terrastruct.com/d2/lib/go2"
"oss.terrastruct.com/d2/lib/textmeasure"
)
// TODO: Refactor with a light abstract layer on top of AST implementing scenarios,

View file

@ -4,7 +4,7 @@ import (
"strings"
"testing"
"oss.terrastruct.com/diff"
"oss.terrastruct.com/util-go/assert"
"oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/d2parser"
@ -44,7 +44,7 @@ func TestKey(t *testing.T) {
if err != nil {
t.Fatal(err)
}
diff.AssertStringEq(t, tc.exp, strings.Join(d2graph.Key(k), "."))
assert.String(t, tc.exp, strings.Join(d2graph.Key(k), "."))
})
}
}

View file

@ -3,7 +3,7 @@ package d2graph
import (
"encoding/json"
"oss.terrastruct.com/d2/lib/go2"
"oss.terrastruct.com/util-go/go2"
)
type SerializedGraph struct {

View file

@ -11,12 +11,13 @@ import (
"cdr.dev/slog"
v8 "rogchap.com/v8go"
"oss.terrastruct.com/xdefer"
"oss.terrastruct.com/util-go/xdefer"
"oss.terrastruct.com/util-go/go2"
"oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/d2target"
"oss.terrastruct.com/d2/lib/geo"
"oss.terrastruct.com/d2/lib/go2"
"oss.terrastruct.com/d2/lib/label"
"oss.terrastruct.com/d2/lib/log"
"oss.terrastruct.com/d2/lib/shape"

View file

@ -13,12 +13,13 @@ import (
"rogchap.com/v8go"
"oss.terrastruct.com/xdefer"
"oss.terrastruct.com/util-go/xdefer"
"oss.terrastruct.com/util-go/go2"
"oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/d2target"
"oss.terrastruct.com/d2/lib/geo"
"oss.terrastruct.com/d2/lib/go2"
"oss.terrastruct.com/d2/lib/label"
)

View file

@ -1,6 +1,6 @@
//go:build cgo
package d2
package d2lib
import "oss.terrastruct.com/d2/d2layouts/d2dagrelayout"

View file

@ -1,4 +1,4 @@
package d2
package d2lib
import (
"context"
@ -9,9 +9,8 @@ import (
"oss.terrastruct.com/d2/d2compiler"
"oss.terrastruct.com/d2/d2exporter"
"oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/d2layouts/d2sequence"
"oss.terrastruct.com/d2/d2renderers/textmeasure"
"oss.terrastruct.com/d2/d2target"
"oss.terrastruct.com/d2/lib/textmeasure"
)
type CompileOptions struct {
@ -23,7 +22,7 @@ type CompileOptions struct {
ThemeID int64
}
func Compile(ctx context.Context, input string, opts *CompileOptions) (*d2target.Diagram, error) {
func Compile(ctx context.Context, input string, opts *CompileOptions) (*d2target.Diagram, *d2graph.Graph, error) {
if opts == nil {
opts = &CompileOptions{}
}
@ -32,12 +31,12 @@ func Compile(ctx context.Context, input string, opts *CompileOptions) (*d2target
UTF16: opts.UTF16,
})
if err != nil {
return nil, err
return nil, nil, err
}
err = g.SetDimensions(opts.MeasuredTexts, opts.Ruler)
if err != nil {
return nil, err
return nil, nil, err
}
if layout, err := getLayout(opts); err != nil {
@ -47,7 +46,7 @@ func Compile(ctx context.Context, input string, opts *CompileOptions) (*d2target
}
diagram, err := d2exporter.Export(ctx, g, opts.ThemeID)
return diagram, err
return diagram, g, err
}
func getLayout(opts *CompileOptions) (func(context.Context, *d2graph.Graph) error, error) {

View file

@ -7,9 +7,11 @@ import (
"strings"
"unicode"
"oss.terrastruct.com/xdefer"
"oss.terrastruct.com/util-go/xdefer"
"oss.terrastruct.com/xrand"
"oss.terrastruct.com/util-go/xrand"
"oss.terrastruct.com/util-go/go2"
"oss.terrastruct.com/d2/d2ast"
"oss.terrastruct.com/d2/d2compiler"
@ -17,7 +19,6 @@ import (
"oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/d2parser"
"oss.terrastruct.com/d2/d2target"
"oss.terrastruct.com/d2/lib/go2"
)
func Create(g *d2graph.Graph, key string) (_ *d2graph.Graph, newKey string, err error) {

View file

@ -7,18 +7,16 @@ import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
"oss.terrastruct.com/xjson"
"oss.terrastruct.com/diff"
"oss.terrastruct.com/util-go/assert"
"oss.terrastruct.com/util-go/diff"
"oss.terrastruct.com/util-go/go2"
"oss.terrastruct.com/util-go/xjson"
"oss.terrastruct.com/d2/d2compiler"
"oss.terrastruct.com/d2/d2format"
"oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/d2oracle"
"oss.terrastruct.com/d2/d2target"
"oss.terrastruct.com/d2/lib/go2"
)
// TODO: make assertions less specific
@ -966,10 +964,10 @@ z: {
}
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
assert.Equal(t, 3, len(g.Objects))
assert.Equal(t, 1, len(g.Edges))
assert.Equal(t, "q", g.Edges[0].Src.ID)
assert.Equal(t, "0.4", g.Edges[0].Attributes.Style.Opacity.Value)
assert.JSON(t, 3, len(g.Objects))
assert.JSON(t, 1, len(g.Edges))
assert.JSON(t, "q", g.Edges[0].Src.ID)
assert.JSON(t, "0.4", g.Edges[0].Attributes.Style.Opacity.Value)
},
},
{
@ -1617,8 +1615,8 @@ func TestMove(t *testing.T) {
exp: `b
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
assert.Equal(t, len(g.Objects), 1)
assert.Equal(t, g.Objects[0].ID, "b")
assert.JSON(t, len(g.Objects), 1)
assert.JSON(t, g.Objects[0].ID, "b")
},
},
{
@ -1636,8 +1634,8 @@ func TestMove(t *testing.T) {
}
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
assert.Equal(t, len(g.Objects), 2)
assert.Equal(t, g.Objects[1].ID, "c")
assert.JSON(t, len(g.Objects), 2)
assert.JSON(t, g.Objects[1].ID, "c")
},
},
{
@ -1692,9 +1690,9 @@ c
}
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
assert.Equal(t, len(g.Objects), 3)
assert.Equal(t, "a", g.Objects[0].ID)
assert.Equal(t, 2, len(g.Objects[0].Children))
assert.JSON(t, len(g.Objects), 3)
assert.JSON(t, "a", g.Objects[0].ID)
assert.JSON(t, 2, len(g.Objects[0].Children))
},
},
{
@ -1733,9 +1731,9 @@ c
}
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
assert.Equal(t, len(g.Objects), 2)
assert.Equal(t, "a", g.Objects[0].ID)
assert.Equal(t, 1, len(g.Objects[0].Children))
assert.JSON(t, len(g.Objects), 2)
assert.JSON(t, "a", g.Objects[0].ID)
assert.JSON(t, 1, len(g.Objects[0].Children))
},
},
{
@ -1752,9 +1750,9 @@ c
b
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
assert.Equal(t, len(g.Objects), 2)
assert.Equal(t, "a", g.Objects[0].ID)
assert.Equal(t, 0, len(g.Objects[0].Children))
assert.JSON(t, len(g.Objects), 2)
assert.JSON(t, "a", g.Objects[0].ID)
assert.JSON(t, 0, len(g.Objects[0].Children))
},
},
{
@ -1863,11 +1861,11 @@ c: {
}
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
assert.Equal(t, len(g.Objects), 3)
assert.Equal(t, "a", g.Objects[0].ID)
assert.Equal(t, 0, len(g.Objects[0].Children))
assert.Equal(t, "c", g.Objects[1].ID)
assert.Equal(t, 1, len(g.Objects[1].Children))
assert.JSON(t, len(g.Objects), 3)
assert.JSON(t, "a", g.Objects[0].ID)
assert.JSON(t, 0, len(g.Objects[0].Children))
assert.JSON(t, "c", g.Objects[1].ID)
assert.JSON(t, 1, len(g.Objects[1].Children))
},
},
{
@ -1929,7 +1927,7 @@ a: {
}
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
assert.Equal(t, len(g.Objects), 3)
assert.JSON(t, len(g.Objects), 3)
},
},
{
@ -1986,7 +1984,7 @@ c: {
}
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
assert.Equal(t, len(g.Objects), 3)
assert.JSON(t, len(g.Objects), 3)
},
},
{
@ -2004,7 +2002,7 @@ d: {
}
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
assert.Equal(t, len(g.Objects), 4)
assert.JSON(t, len(g.Objects), 4)
},
},
{
@ -2023,7 +2021,7 @@ c: {
}
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
assert.Equal(t, len(g.Objects), 4)
assert.JSON(t, len(g.Objects), 4)
},
},
{
@ -4422,10 +4420,8 @@ func (tc editTest) run(t *testing.T) {
Err: fmt.Sprintf("%#v", err),
}
err = diff.Testdata(filepath.Join("..", "testdata", "d2oracle", t.Name()), got)
if err != nil {
t.Fatal(err)
}
err = diff.TestdataJSON(filepath.Join("..", "testdata", "d2oracle", t.Name()), got)
assert.Success(t, err)
}
func TestMoveIDDeltas(t *testing.T) {
@ -4635,7 +4631,7 @@ x.a -> x.b
t.Fatal(err)
}
ds, err := diff.Strings(tc.exp, xjson.MarshalIndent(deltas))
ds, err := diff.Strings(tc.exp, string(xjson.Marshal(deltas)))
if err != nil {
t.Fatal(err)
}
@ -4825,7 +4821,7 @@ x.y.z.w.e.p.l -> x.y.z.1.2.3.4
t.Fatal(err)
}
ds, err := diff.Strings(tc.exp, xjson.MarshalIndent(deltas))
ds, err := diff.Strings(tc.exp, string(xjson.Marshal(deltas)))
if err != nil {
t.Fatal(err)
}
@ -4977,7 +4973,7 @@ x.y.z.w.e.p.l -> x.y.z.1.2.3.4
t.Fatal(err)
}
ds, err := diff.Strings(tc.exp, xjson.MarshalIndent(deltas))
ds, err := diff.Strings(tc.exp, string(xjson.Marshal(deltas)))
if err != nil {
t.Fatal(err)
}

View file

@ -9,8 +9,9 @@ import (
"unicode"
"unicode/utf8"
"oss.terrastruct.com/util-go/go2"
"oss.terrastruct.com/d2/d2ast"
"oss.terrastruct.com/d2/lib/go2"
)
type ParseOptions struct {

View file

@ -6,7 +6,8 @@ import (
"strings"
"testing"
"oss.terrastruct.com/diff"
"oss.terrastruct.com/util-go/assert"
"oss.terrastruct.com/util-go/diff"
"oss.terrastruct.com/d2/d2ast"
"oss.terrastruct.com/d2/d2parser"
@ -382,10 +383,8 @@ q.(x -> y).z: (rawr)
Err: err,
}
err = diff.Testdata(filepath.Join("..", "testdata", "d2parser", t.Name()), got)
if err != nil {
t.Fatal(err)
}
err = diff.TestdataJSON(filepath.Join("..", "testdata", "d2parser", t.Name()), got)
assert.Success(t, err)
})
}
}

View file

@ -9,7 +9,7 @@ import (
"os/exec"
"time"
"oss.terrastruct.com/xdefer"
"oss.terrastruct.com/util-go/xdefer"
"oss.terrastruct.com/d2/d2graph"
)

View file

@ -9,8 +9,9 @@ import (
"context"
"os/exec"
"oss.terrastruct.com/util-go/xexec"
"oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/lib/xexec"
)
// plugins contains the bundled d2 plugins.

View file

@ -7,8 +7,9 @@ import (
"fmt"
"io"
"oss.terrastruct.com/util-go/xmain"
"oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/lib/xmain"
)
// Serve returns a xmain.RunFunc that will invoke the plugin p as necessary to service the

View file

@ -9,7 +9,7 @@ import (
"regexp"
"strconv"
"oss.terrastruct.com/xdefer"
"oss.terrastruct.com/util-go/xdefer"
v8 "rogchap.com/v8go"
)

View file

@ -20,15 +20,16 @@ import (
"github.com/alecthomas/chroma/lexers"
"github.com/alecthomas/chroma/styles"
"oss.terrastruct.com/util-go/go2"
"oss.terrastruct.com/d2/d2renderers/d2fonts"
"oss.terrastruct.com/d2/d2renderers/d2latex"
"oss.terrastruct.com/d2/d2renderers/textmeasure"
"oss.terrastruct.com/d2/d2target"
"oss.terrastruct.com/d2/lib/color"
"oss.terrastruct.com/d2/lib/geo"
"oss.terrastruct.com/d2/lib/go2"
"oss.terrastruct.com/d2/lib/label"
"oss.terrastruct.com/d2/lib/shape"
"oss.terrastruct.com/d2/lib/textmeasure"
)
const (

View file

@ -5,9 +5,10 @@ import (
"net/url"
"strings"
"oss.terrastruct.com/util-go/go2"
"oss.terrastruct.com/d2/d2themes"
"oss.terrastruct.com/d2/lib/geo"
"oss.terrastruct.com/d2/lib/go2"
"oss.terrastruct.com/d2/lib/label"
"oss.terrastruct.com/d2/lib/shape"
)

View file

@ -1,15 +1,16 @@
# install
You may install D2 through any of the following methods.
You may install `d2` through any of the following methods.
<!-- toc -->
- [install.sh](#installsh)
- [macOS (Homebrew)](#macos-homebrew)
- [Standalone](#standalone)
- [From source](#from-source)
<!-- tocstop -->
- <a href="#installsh" id="toc-installsh">install.sh</a>
- <a href="#security" id="toc-security">Security</a>
- <a href="#macos-homebrew" id="toc-macos-homebrew">macOS (Homebrew)</a>
- <a href="#standalone" id="toc-standalone">Standalone</a>
- <a href="#manual" id="toc-manual">Manual</a>
- <a href="#prefix" id="toc-prefix">PREFIX</a>
- <a href="#from-source" id="toc-from-source">From source</a>
- <a href="#coming-soon" id="toc-coming-soon">Coming soon</a>
## install.sh
@ -31,6 +32,36 @@ methods:
curl -fsSL https://d2lang.com/install.sh | sh -s -- --help
```
### Security
The install script is not the most secure way to install d2. We recommend that if
possible, you use your OS's package manager directly or install from source with `go` as
described below.
But this does not mean the install script is insecure. There is no major flaw that
the install script is more vulnerable to than any other method of manual installation.
The most secure installation method involves a second independent entity, i.e your OS
package repos or Go's proxy server.
We're careful shell programmers and are aware of the many footguns of the Unix shell. Our
script was written carefully and with detail. For example, it is not vulnerable to partial
execution and the entire script runs with `set -eu` and very meticulous quoting.
It follows the XDG standards, installs `d2` properly into a Unix hierarchy path (defaulting
to /usr/local though you can use ~/.local to avoid sudo if you'd like) and allows for easy
uninstall.
Some other niceties are that it'll tell you if you need to adjust `$PATH` or `$MANPATH` to
access `d2` and its manpages. It can also install
[TALA](https://github.com/terrastruct/tala) for you with `--tala`. You can also use it to
install a specific version of `d2` with `--version`. Run it with `--help` for more more
detailed docs on its various options and features.
If you're still concerned, remember you can run with `--dry-run` to avoid writing anything.
The install script does not yet verify any signature on the downloaded release
but that is coming soon. [#315](https://github.com/terrastruct/d2/issues/315)
## macOS (Homebrew)
If you're on macOS, you can install with `brew`.
@ -46,8 +77,28 @@ brew install d2
## Standalone
We publish standalone release archives for every release on Github.
Download the `.tar.gz` release for your OS/ARCH combination and then run the following
inside the extracted directory to install:
Here's a minimal example script that downloads a standalone release, extracts it into the
current directory and then installs it.
Adjust VERSION, OS, and ARCH as needed.
```sh
VERSION=v0.0.13 OS=macos ARCH=amd64 curl -fsSLO \
"https://github.com/terrastruct/d2/releases/download/$VERSION/d2-$VERSION-$OS-$ARCH.tar.gz" \
&& tar -xzf "d2-$VERSION-$OS-$ARCH.tar.gz" \
&& make -sC "d2-$VERSION" install
```
To uninstall:
```sh
VERSION=v0.0.13 make -sC "d2-$VERSION" uninstall
```
### Manual
You can also manually download the `.tar.gz` release for your OS/ARCH combination and then
run the following inside the extracted directory to install:
```sh
make install
@ -59,10 +110,11 @@ Run the following to uninstall:
make uninstall
```
If root permissions are required for installation, you'll need to run `make` with `sudo`.
### PREFIX
You can control the Unix hierarchy installation path with `PREFIX=`. For example:
```
```sh
# Install under ~/.local.
# Binaries will be at ~/.local/bin
# And manpages will be under ~/.local/share/man
@ -83,7 +135,15 @@ know where the release directory is for easy uninstall.
You can always install from source:
```sh
go install oss.terrastruct.com/d2/cmd/d2@latest
go install oss.terrastruct.com/d2@latest
```
To install a proper release from source clone the repository and then:
```sh
./ci/release/build.sh --install
# To uninstall:
# ./ci/release/build.sh --uninstall
```
## Coming soon

1
docs/examples/lib/1-d2lib/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
out.svg

View file

@ -0,0 +1,25 @@
package main
import (
"context"
"io/ioutil"
"path/filepath"
"oss.terrastruct.com/d2/d2layouts/d2dagrelayout"
"oss.terrastruct.com/d2/d2lib"
"oss.terrastruct.com/d2/d2renderers/d2svg"
"oss.terrastruct.com/d2/d2themes/d2themescatalog"
"oss.terrastruct.com/d2/lib/textmeasure"
)
// Remember to add if err != nil checks in production.
func main() {
ruler, _ := textmeasure.NewRuler()
diagram, _, _ := d2lib.Compile(context.Background(), "x -> y", &d2lib.CompileOptions{
Layout: d2dagrelayout.Layout,
Ruler: ruler,
ThemeID: d2themescatalog.GrapeSoda.ID,
})
out, _ := d2svg.Render(diagram)
_ = ioutil.WriteFile(filepath.Join("out.svg"), out, 0600)
}

View file

@ -0,0 +1,9 @@
package main
import (
"testing"
)
func TestMain_(t *testing.T) {
main()
}

View file

@ -0,0 +1,36 @@
package main
import (
"context"
"fmt"
"oss.terrastruct.com/d2/d2format"
"oss.terrastruct.com/d2/d2layouts/d2dagrelayout"
"oss.terrastruct.com/d2/d2lib"
"oss.terrastruct.com/d2/d2oracle"
"oss.terrastruct.com/d2/d2themes/d2themescatalog"
"oss.terrastruct.com/d2/lib/textmeasure"
)
// Remember to add if err != nil checks in production.
func main() {
// From one.go
ruler, _ := textmeasure.NewRuler()
_, graph, _ := d2lib.Compile(context.Background(), "x -> y", &d2lib.CompileOptions{
Layout: d2dagrelayout.Layout,
Ruler: ruler,
ThemeID: d2themescatalog.GrapeSoda.ID,
})
// Create a shape with the ID, "meow"
graph, _, _ = d2oracle.Create(graph, "meow")
// Style the shape green
color := "green"
graph, _ = d2oracle.Set(graph, "meow.style.fill", nil, &color)
// Create a shape with the ID, "cat"
graph, _, _ = d2oracle.Create(graph, "cat")
// Move the shape "meow" inside the container "cat"
graph, _ = d2oracle.Move(graph, "meow", "cat.meow")
// Prints formatted D2 script
fmt.Print(d2format.Format(graph.AST))
}

View file

@ -0,0 +1,9 @@
package main
import (
"testing"
)
func TestMain_(t *testing.T) {
main()
}

View file

@ -0,0 +1 @@
out.svg

View file

@ -0,0 +1,26 @@
package main
import (
"context"
"io/ioutil"
"path/filepath"
"strings"
"oss.terrastruct.com/d2/d2compiler"
"oss.terrastruct.com/d2/d2exporter"
"oss.terrastruct.com/d2/d2layouts/d2dagrelayout"
"oss.terrastruct.com/d2/d2renderers/d2svg"
"oss.terrastruct.com/d2/d2themes/d2themescatalog"
"oss.terrastruct.com/d2/lib/textmeasure"
)
// Remember to add if err != nil checks in production.
func main() {
graph, _ := d2compiler.Compile("", strings.NewReader("x -> y"), nil)
ruler, _ := textmeasure.NewRuler()
_ = graph.SetDimensions(nil, ruler)
_ = d2dagrelayout.Layout(context.Background(), graph)
diagram, _ := d2exporter.Export(context.Background(), graph, d2themescatalog.NeutralDefault.ID)
out, _ := d2svg.Render(diagram)
_ = ioutil.WriteFile(filepath.Join("out.svg"), out, 0600)
}

View file

@ -0,0 +1,9 @@
package main
import (
"testing"
)
func TestMain_(t *testing.T) {
main()
}

View file

@ -0,0 +1,30 @@
# D2 library examples
We have a few examples in this directory on how to use the D2 library to turn D2 scripts
into rendered svg diagrams and more.
Each example is runnable though does not include error handling for readability.
### [./1-d2lib](./1-d2lib)
A minimal example showing you how to compile the diagram `x -> y` into an svg.
### [./2-d2oracle](./2-d2oracle)
D2 is built to be hackable -- the language has an API built on top of it to make edits
programmatically.
Modifying the previous example, this example demonstrates how
[d2oracle](../../../d2oracle) can be used to create a new shape, style it programatically
and then output the modified d2 script.
This makes it easy to build functionality on top of D2. Terrastruct uses the
[d2oracle](../../../d2oracle) API to implement editing of D2 from mouse actions in a
visual interface.
### [./3-lowlevel](./3-lowlevel)
`d2lib` from the first example is just a wrapper around the lower level APIs. They
can be used directly and this example demonstrates such usage.
This shouldn't be necessary for most usecases.

View file

@ -12,19 +12,19 @@ import (
"cdr.dev/slog"
"github.com/stretchr/testify/assert"
tassert "github.com/stretchr/testify/assert"
"oss.terrastruct.com/diff"
"oss.terrastruct.com/util-go/assert"
"oss.terrastruct.com/util-go/diff"
"oss.terrastruct.com/d2"
"oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/d2layouts/d2dagrelayout"
"oss.terrastruct.com/d2/d2layouts/d2elklayout"
"oss.terrastruct.com/d2/d2lib"
"oss.terrastruct.com/d2/d2renderers/d2svg"
"oss.terrastruct.com/d2/d2renderers/textmeasure"
"oss.terrastruct.com/d2/d2target"
xdiff "oss.terrastruct.com/d2/lib/diff"
"oss.terrastruct.com/d2/lib/log"
"oss.terrastruct.com/d2/lib/textmeasure"
)
func TestE2E(t *testing.T) {
@ -90,7 +90,7 @@ func run(t *testing.T, tc testCase) {
ctx = log.Leveled(ctx, slog.LevelDebug)
ruler, err := textmeasure.NewRuler()
if !assert.Nil(t, err) {
if !tassert.Nil(t, err) {
return
}
@ -104,13 +104,13 @@ func run(t *testing.T, tc testCase) {
} else if layoutName == "elk" {
layout = d2elklayout.Layout
}
diagram, err := d2.Compile(ctx, tc.script, &d2.CompileOptions{
diagram, _, err := d2lib.Compile(ctx, tc.script, &d2lib.CompileOptions{
UTF16: true,
Ruler: ruler,
ThemeID: 0,
Layout: layout,
})
if !assert.Nil(t, err) {
if !tassert.Nil(t, err) {
return
}
@ -122,34 +122,23 @@ func run(t *testing.T, tc testCase) {
dataPath := filepath.Join("testdata", strings.TrimPrefix(t.Name(), "TestE2E/"), layoutName)
pathGotSVG := filepath.Join(dataPath, "sketch.got.svg")
pathExpSVG := filepath.Join(dataPath, "sketch.exp.svg")
svgBytes, err := d2svg.Render(diagram)
if err != nil {
t.Fatal(err)
}
assert.Success(t, err)
err = ioutil.WriteFile(pathGotSVG, svgBytes, 0600)
assert.Success(t, err)
defer os.Remove(pathGotSVG)
var xmlParsed interface{}
if err := xml.Unmarshal(svgBytes, &xmlParsed); err != nil {
t.Fatalf("invalid SVG: %v", err)
}
err = xml.Unmarshal(svgBytes, &xmlParsed)
assert.Success(t, err)
err = diff.Testdata(filepath.Join(dataPath, "board"), diagram)
if err != nil {
ioutil.WriteFile(pathGotSVG, svgBytes, 0600)
t.Fatal(err)
}
err = diff.TestdataJSON(filepath.Join(dataPath, "board"), diagram)
assert.Success(t, err)
if os.Getenv("SKIP_SVG_CHECK") == "" {
err = xdiff.TestdataGeneric(filepath.Join(dataPath, "sketch"), ".svg", svgBytes)
if err != nil {
ioutil.WriteFile(pathGotSVG, svgBytes, 0600)
t.Fatal(err)
}
err = diff.Testdata(filepath.Join(dataPath, "sketch"), ".svg", svgBytes)
assert.Success(t, err)
}
err = ioutil.WriteFile(pathExpSVG, svgBytes, 0600)
if err != nil {
t.Fatal(err)
}
os.Remove(filepath.Join(dataPath, "sketch.got.svg"))
}
}

37
fmt.go Normal file
View file

@ -0,0 +1,37 @@
package main
import (
"bytes"
"context"
"oss.terrastruct.com/util-go/xdefer"
"oss.terrastruct.com/util-go/xmain"
"oss.terrastruct.com/d2/d2format"
"oss.terrastruct.com/d2/d2parser"
)
func fmtCmd(ctx context.Context, ms *xmain.State) (err error) {
defer xdefer.Errorf(&err, "failed to fmt")
ms.Opts = xmain.NewOpts(ms.Env, ms.Log, ms.Opts.Flags.Args()[1:])
if len(ms.Opts.Args) == 0 {
return xmain.UsageErrorf("fmt must be passed the file to be formatted")
} else if len(ms.Opts.Args) > 1 {
return xmain.UsageErrorf("fmt accepts only one argument for the file to be formatted")
}
inputPath := ms.Opts.Args[0]
input, err := ms.ReadPath(inputPath)
if err != nil {
return err
}
m, err := d2parser.Parse(inputPath, bytes.NewReader(input), nil)
if err != nil {
return err
}
return ms.WritePath(inputPath, []byte(d2format.Format(m)))
}

16
go.mod generated
View file

@ -10,25 +10,17 @@ require (
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
github.com/lucasb-eyer/go-colorful v1.2.0
github.com/mazznoer/csscolorparser v0.1.3
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8
github.com/playwright-community/playwright-go v0.2000.1
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.8.1
github.com/yuin/goldmark v1.5.3
go.uber.org/multierr v1.8.0
golang.org/x/exp v0.0.0-20221126150942-6ab00d035af9
golang.org/x/image v0.1.0
golang.org/x/net v0.2.0
golang.org/x/text v0.4.0
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2
gonum.org/v1/plot v0.12.0
nhooyr.io/websocket v1.8.7
oss.terrastruct.com/cmdlog v0.0.0-20221129200109-540ef52ff07d
oss.terrastruct.com/diff v1.0.2-0.20221116222035-8bf4dd3ab541
oss.terrastruct.com/xcontext v0.0.0-20221018000442-50fdafb12f4f
oss.terrastruct.com/xdefer v0.0.0-20221017222355-6f3b6e4d1557
oss.terrastruct.com/xjson v0.0.0-20221018000420-4986731c4c4a
oss.terrastruct.com/xos v0.0.0-20221018030138-c96e7ae96e5d
oss.terrastruct.com/xrand v0.0.0-20221020211818-4ac08e618333
oss.terrastruct.com/util-go v0.0.0-20221201191904-5edc89ce397b
rogchap.com/v8go v0.7.1-0.20221102201510-1f00b5007d95
)
@ -49,15 +41,17 @@ require (
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rogpeppe/go-internal v1.8.0 // indirect
github.com/ugorji/go/codec v1.2.6 // indirect
go.opencensus.io v0.24.0 // indirect
go.uber.org/atomic v1.7.0 // indirect
golang.org/x/crypto v0.3.0 // indirect
golang.org/x/exp v0.0.0-20221126150942-6ab00d035af9 // indirect
golang.org/x/sys v0.2.0 // indirect
golang.org/x/term v0.2.0 // indirect
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
golang.org/x/text v0.4.0 // indirect
google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect

16
go.sum generated
View file

@ -798,20 +798,8 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g=
nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=
oss.terrastruct.com/cmdlog v0.0.0-20221129200109-540ef52ff07d h1:oc3cqW4bWaosVpfM8yNLAPSDMjLrVYP2ztF7w3tRvws=
oss.terrastruct.com/cmdlog v0.0.0-20221129200109-540ef52ff07d/go.mod h1:ROL3yxl2X+S3O+Rls00qdX6aMh+p1dF8IdxDRwDDpsg=
oss.terrastruct.com/diff v1.0.2-0.20221116222035-8bf4dd3ab541 h1:I9B1O1IJ6spivIQxbFRZmbhAwVeLwrcQRR1JbYUOvrI=
oss.terrastruct.com/diff v1.0.2-0.20221116222035-8bf4dd3ab541/go.mod h1:ags2QDy/T6jr69hT6bpmAmhr2H98n9o8Atf3QlUJPiU=
oss.terrastruct.com/xcontext v0.0.0-20221018000442-50fdafb12f4f h1:7voRCwKM7TZkTo9u7hj+uV/zXoVB8czWrTq6MVIh3dg=
oss.terrastruct.com/xcontext v0.0.0-20221018000442-50fdafb12f4f/go.mod h1:Y0coTLsWwX0q3a+/Ndq797t+vWyxm42T49Ik3bzaDKY=
oss.terrastruct.com/xdefer v0.0.0-20221017222355-6f3b6e4d1557 h1:rPbhJbN1q7B4tnppSPoAMwq0t6Pk5SrQDQ5S6uoNNHg=
oss.terrastruct.com/xdefer v0.0.0-20221017222355-6f3b6e4d1557/go.mod h1:plvfydF5METAlsbpeuSz44jckaOwrCWX3M0kTLoCA4I=
oss.terrastruct.com/xjson v0.0.0-20221018000420-4986731c4c4a h1:AAcupsjBwpbcyLASX0ppDlxbfHWb5Neq5gWdGpLfaSA=
oss.terrastruct.com/xjson v0.0.0-20221018000420-4986731c4c4a/go.mod h1:XJ71qiTzk/dbTWuYbuLJuRpBdKFN06Sk5FdFpq2TNmE=
oss.terrastruct.com/xos v0.0.0-20221018030138-c96e7ae96e5d h1:rrPTkbAfsRTW1WLoTzEofS9AprsHovy9bwvA/wC8Dys=
oss.terrastruct.com/xos v0.0.0-20221018030138-c96e7ae96e5d/go.mod h1:uSONPDInIwglnC+0zYs8YOjiUD8ZUSnqDTTI82j7Oro=
oss.terrastruct.com/xrand v0.0.0-20221020211818-4ac08e618333 h1:7EdxwXM75Id1VIN71QbE8bLzZRMs0qD7olnDw5gbI7w=
oss.terrastruct.com/xrand v0.0.0-20221020211818-4ac08e618333/go.mod h1:O7TAoBmlQhoi46RdgVikDcoLRb/vLflhkXCAd+nO4SM=
oss.terrastruct.com/util-go v0.0.0-20221201191904-5edc89ce397b h1:o8+5KfZpQyaw7uKcPIdc9HOqVjVDEdsPZpdRV1k0rmc=
oss.terrastruct.com/util-go v0.0.0-20221201191904-5edc89ce397b/go.mod h1:Fwy72FDIOOM4K8F96ScXkxHHppR1CPfUyo9+x9c1PBU=
rogchap.com/v8go v0.7.1-0.20221102201510-1f00b5007d95 h1:r89YHVIWeQj/A3Nu6462eqARUECJlJkLRk36pfML1xA=
rogchap.com/v8go v0.7.1-0.20221102201510-1f00b5007d95/go.mod h1:MxgP3pL2MW4dpme/72QRs8sgNMmM0pRc8DPhcuLWPAs=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=

View file

@ -9,29 +9,37 @@ import (
"path/filepath"
"strings"
"oss.terrastruct.com/util-go/xmain"
"oss.terrastruct.com/d2/d2plugin"
"oss.terrastruct.com/d2/lib/xmain"
)
func help(ms *xmain.State) {
fmt.Fprintf(ms.Stdout, `Usage:
%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 fmt file.d2
%[1]s compiles and renders file.d2 to file.svg | file.png
It defaults to file.svg if an output path is not provided.
%[1]s compiles and renders file.d2 to file.svg|file.png.
Use - to have d2 read from stdin or write to stdout.
See man d2 for more detailed docs.
Flags:
%s
Subcommands:
%[1]s layout - Lists available layout engine options with short help
%[1]s layout [layout name] - Display long help for a particular layout engine
%[1]s layout [name] - Display long help for a particular layout engine
%[1]s fmt file.d2 - Format file.d2
See more docs and the source code at https://oss.terrastruct.com/d2
`, ms.Name, ms.Opts.Defaults())
`, filepath.Base(ms.Name), ms.Opts.Defaults())
}
func layoutHelp(ctx context.Context, ms *xmain.State) error {
func layoutCmd(ctx context.Context, ms *xmain.State) error {
if len(ms.Opts.Flags.Args()) == 1 {
return shortLayoutHelp(ctx, ms)
} else if len(ms.Opts.Flags.Args()) == 2 {

View file

@ -61,22 +61,26 @@ tput() {
should_color() {
if [ -n "${COLOR-}" ]; then
if [ "$COLOR" = 0 -o "$COLOR" = false ]; then
_COLOR=
return 1
elif [ "$COLOR" = 1 -o "$COLOR" = true ]; then
if [ "$COLOR" = 1 -o "$COLOR" = true ]; then
_COLOR=1
__COLOR=1
return 0
elif [ "$COLOR" = 0 -o "$COLOR" = false ]; then
_COLOR=
__COLOR=0
return 1
else
printf '$COLOR must be 0, 1, false or true but got %s\n' "$COLOR" >&2
fi
fi
if [ -t 1 ]; then
if [ -t 1 -a "${TERM-}" != dumb ]; then
_COLOR=1
__COLOR=1
return 0
else
_COLOR=
__COLOR=0
return 1
fi
}
@ -95,7 +99,8 @@ _echo() {
get_rand_color() {
# 1-6 are regular and 9-14 are bright.
# 1,2 and 9,10 are red and green but we use those for success and failure.
pick "$*" 3 4 5 6 11 12 13 14
pick "$*" 1 2 3 4 5 6 \
9 10 11 12 13 14
}
echop() {
@ -119,9 +124,9 @@ printfp() {(
fi
should_color || true
if [ $# -eq 0 ]; then
printf '%s' "$(COLOR=${_COLOR-} setaf "$FGCOLOR" "$prefix")"
printf '%s' "$(COLOR=$__COLOR setaf "$FGCOLOR" "$prefix")"
else
printf '%s: %s\n' "$(COLOR=${_COLOR-} setaf "$FGCOLOR" "$prefix")" "$(printf "$@")"
printf '%s: %s\n' "$(COLOR=$__COLOR setaf "$FGCOLOR" "$prefix")" "$(printf "$@")"
fi
)}
@ -130,7 +135,7 @@ catp() {
shift
should_color || true
sed "s/^/$(COLOR=${_COLOR-} printfp "$prefix" '')/"
sed "s/^/$(COLOR=$__COLOR printfp "$prefix" '')/"
}
repeat() {
@ -157,17 +162,17 @@ printferr() {
logp() {
should_color >&2 || true
COLOR=${_COLOR-} echop "$@" | humanpath >&2
COLOR=$__COLOR echop "$@" | humanpath >&2
}
logfp() {
should_color >&2 || true
COLOR=${_COLOR-} printfp "$@" | humanpath >&2
COLOR=$__COLOR printfp "$@" | humanpath >&2
}
logpcat() {
should_color >&2 || true
COLOR=${_COLOR-} catp "$@" | humanpath >&2
COLOR=$__COLOR catp "$@" | humanpath >&2
}
log() {
@ -292,6 +297,13 @@ runtty() {
return 1
esac
}
capcode() {
set +e
"$@"
code=$?
set -e
}
#!/bin/sh
if [ "${LIB_FLAG-}" ]; then
return 0
@ -468,6 +480,14 @@ manpath() {
echo "${MANPATH-}"
fi
}
is_writable_dir() {
# The path has to exist for -w to succeed.
sh_c "mkdir -p '$1' 2>/dev/null" || true
if [ ! -w "$1" ]; then
return 1
fi
}
#!/bin/sh
set -eu
@ -556,6 +576,9 @@ note: Deleting the unarchived releases will cause --uninstall to stop working.
You can rerun install.sh to update your version of D2. install.sh will avoid reinstalling
if the installed version is the latest unless --force is passed.
See https://github.com/terrastruct/d2/blob/master/docs/INSTALL.md#security for
documentation on its security.
EOF
}
@ -915,13 +938,10 @@ uninstall_tala_brew() {
}
is_prefix_writable() {
sh_c "mkdir -p '$INSTALL_DIR' 2>/dev/null" || true
# The reason for checking whether $INSTALL_DIR is writable is that on macOS you have
# /usr/local owned by root but you don't need root to write to its subdirectories which
# is all we want to do.
if [ ! -w "$INSTALL_DIR" ]; then
return 1
fi
is_writable_dir "$INSTALL_DIR"
}
cache_dir() {
@ -974,4 +994,7 @@ brew() {
HOMEBREW_NO_INSTALL_CLEANUP=1 HOMEBREW_NO_AUTO_UPDATE=1 command brew "$@"
}
# The main function does more than provide organization. It provides robustness in that if
# the install script was to only partial download into sh, sh will not execute it because
# main is not invoked until the very last byte.
main "$@"

View file

@ -1,27 +0,0 @@
package compress
import (
"testing"
"oss.terrastruct.com/diff"
)
func TestCompression(t *testing.T) {
script := `x -> y
I just forgot my whole philosophy of life!!!: {
s: TV is chewing gum for the eyes
}
`
encoded, err := Compress(script)
if err != nil {
t.Fatal(err)
}
decoded, err := Decompress(encoded)
if err != nil {
t.Fatal(err)
}
diff.AssertStringEq(t, script, decoded)
}

View file

@ -1,38 +0,0 @@
package diff
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"oss.terrastruct.com/diff"
)
// TODO refactor with diff repo
func TestdataGeneric(path, fileExtension string, got []byte) (err error) {
expPath := fmt.Sprintf("%s.exp%s", path, fileExtension)
gotPath := fmt.Sprintf("%s.got%s", path, fileExtension)
err = os.MkdirAll(filepath.Dir(gotPath), 0755)
if err != nil {
return err
}
err = ioutil.WriteFile(gotPath, got, 0600)
if err != nil {
return err
}
ds, err := diff.Files(expPath, gotPath)
if err != nil {
return err
}
if ds != "" {
if os.Getenv("TESTDATA_ACCEPT") != "" {
return os.Rename(gotPath, expPath)
}
return fmt.Errorf("diff (rerun with $TESTDATA_ACCEPT=1 to accept):\n%s", ds)
}
return os.Remove(gotPath)
}

View file

@ -1,60 +0,0 @@
// Package go2 contains general utility helpers that should've been in Go. Maybe they'll be in Go 2.0.
package go2
import (
"hash/fnv"
"math"
"golang.org/x/exp/constraints"
)
func Pointer[T any](v T) *T {
return &v
}
func Min[T constraints.Ordered](a, b T) T {
if a < b {
return a
}
return b
}
func Max[T constraints.Ordered](a, b T) T {
if a > b {
return a
}
return b
}
func StringToIntHash(s string) int {
h := fnv.New32a()
h.Write([]byte(s))
return int(h.Sum32())
}
func Contains[T comparable](els []T, el T) bool {
for _, el2 := range els {
if el2 == el {
return true
}
}
return false
}
func Filter[T any](els []T, fn func(T) bool) []T {
out := []T{}
for _, el := range els {
if fn(el) {
out = append(out, el)
}
}
return out
}
func IntMax(x, y int) int {
return int(math.Max(float64(x), float64(y)))
}
func IntMin(x, y int) int {
return int(math.Min(float64(x), float64(y)))
}

View file

@ -6,156 +6,204 @@ import (
"encoding/base64"
"fmt"
"io/ioutil"
"mime"
"net/http"
"net/url"
"os"
"path"
"regexp"
"strings"
"sync"
"time"
"go.uber.org/multierr"
"oss.terrastruct.com/xdefer"
"golang.org/x/xerrors"
"oss.terrastruct.com/util-go/xdefer"
"oss.terrastruct.com/d2/lib/xmain"
"oss.terrastruct.com/util-go/xmain"
)
const maxImageSize int64 = 1 << 25 // 33_554_432
var imageRe = regexp.MustCompile(`<image href="([^"]+)"`)
var imageRegex = regexp.MustCompile(`<image href="([^"]+)"`)
type resp struct {
srctxt string
data string
err error
func BundleLocal(ctx context.Context, ms *xmain.State, in []byte) ([]byte, error) {
return bundle(ctx, ms, in, false)
}
func InlineLocal(ctx context.Context, ms *xmain.State, in []byte) ([]byte, error) {
return inline(ctx, ms, in, false)
func BundleRemote(ctx context.Context, ms *xmain.State, in []byte) ([]byte, error) {
return bundle(ctx, ms, in, true)
}
func InlineRemote(ctx context.Context, ms *xmain.State, in []byte) ([]byte, error) {
return inline(ctx, ms, in, true)
type repl struct {
from []byte
to []byte
}
func inline(ctx context.Context, ms *xmain.State, svg []byte, isRemote bool) (_ []byte, err error) {
defer xdefer.Errorf(&err, "failed to bundle images")
imgs := imageRe.FindAllSubmatch(svg, -1)
var filtered [][][]byte
for _, img := range imgs {
u, err := url.Parse(string(img[1]))
isRemoteImg := err == nil && strings.HasPrefix(u.Scheme, "http")
if isRemoteImg == isRemote {
filtered = append(filtered, img)
}
func bundle(ctx context.Context, ms *xmain.State, svg []byte, isRemote bool) (_ []byte, err error) {
if isRemote {
defer xdefer.Errorf(&err, "failed to bundle remote images")
} else {
defer xdefer.Errorf(&err, "failed to bundle local images")
}
var wg sync.WaitGroup
respChan := make(chan resp)
// Limits the number of workers to 16.
sema := make(chan struct{}, 16)
imgs := imageRegex.FindAllSubmatch(svg, -1)
imgs = filterImageElements(imgs, isRemote)
ctx, cancel := context.WithTimeout(ctx, time.Minute*5)
defer cancel()
wg.Add(len(filtered))
return runWorkers(ctx, ms, svg, imgs, isRemote)
}
// filterImageElements finds all image elements in imgs that are eligible
// for bundling in the current context.
func filterImageElements(imgs [][][]byte, isRemote bool) [][][]byte {
imgs2 := imgs[:0]
for _, img := range imgs {
href := string(img[1])
// Skip already bundled images.
if strings.HasPrefix(href, "data:") {
continue
}
u, err := url.Parse(href)
isRemoteImg := err == nil && strings.HasPrefix(u.Scheme, "http")
if isRemoteImg == isRemote {
imgs2 = append(imgs2, img)
}
}
return imgs2
}
func runWorkers(ctx context.Context, ms *xmain.State, svg []byte, imgs [][][]byte, isRemote bool) (_ []byte, err error) {
var wg sync.WaitGroup
replc := make(chan repl)
wg.Add(len(imgs))
go func() {
wg.Wait()
close(replc)
}()
// Limits the number of workers to 16.
sema := make(chan struct{}, 16)
var errhrefsMu sync.Mutex
var errhrefs []string
// Start workers as the sema allows.
go func() {
for _, img := range filtered {
for _, img := range imgs {
img := img
sema <- struct{}{}
go func(src, href string) {
go func() {
defer func() {
wg.Done()
<-sema
}()
var data string
var err error
if isRemote {
data, err = fetch(ctx, href)
} else {
data, err = read(href)
bundledImage, err := worker(ctx, img[1], isRemote)
if err != nil {
ms.Log.Error.Printf("failed to bundle %s: %v", img[1], err)
errhrefsMu.Lock()
errhrefs = append(errhrefs, string(img[1]))
errhrefsMu.Unlock()
return
}
select {
case <-ctx.Done():
case respChan <- resp{
srctxt: src,
data: data,
err: err,
case replc <- repl{
from: img[0],
to: bundledImage,
}:
}
}(string(img[0]), string(img[1]))
}()
}
}()
go func() {
wg.Wait()
close(respChan)
}()
t := time.NewTicker(time.Second * 5)
defer t.Stop()
for {
select {
case <-ctx.Done():
return nil, fmt.Errorf("failed to wait for imgbundler workers: %w", ctx.Err())
case <-time.After(time.Second * 5):
return svg, xerrors.Errorf("failed to wait for workers: %w", ctx.Err())
case <-t.C:
ms.Log.Info.Printf("fetching images...")
case resp, ok := <-respChan:
case repl, ok := <-replc:
if !ok {
return svg, err
if len(errhrefs) > 0 {
return svg, xerrors.Errorf("%v", errhrefs)
}
return svg, nil
}
if resp.err != nil {
err = multierr.Combine(err, resp.err)
continue
}
svg = bytes.Replace(svg, []byte(resp.srctxt), []byte(fmt.Sprintf(`<image href="%s"`, resp.data)), 1)
svg = bytes.Replace(svg, repl.from, repl.to, 1)
}
}
}
var imgClient = &http.Client{}
func worker(ctx context.Context, href []byte, isRemote bool) ([]byte, error) {
var buf []byte
var mimeType string
var err error
if isRemote {
buf, mimeType, err = httpGet(ctx, string(href))
} else {
buf, err = os.ReadFile(string(href))
}
if err != nil {
return nil, err
}
func fetch(ctx context.Context, href string) (string, error) {
if mimeType == "" {
mimeType = sniffMimeType(href, buf, isRemote)
}
mimeType = strings.Replace(mimeType, "text/xml", "image/svg+xml", 1)
b64 := base64.StdEncoding.EncodeToString(buf)
return []byte(fmt.Sprintf(`<image href="data:%s;base64,%s"`, mimeType, b64)), nil
}
var httpClient = &http.Client{}
func httpGet(ctx context.Context, href string) ([]byte, string, error) {
ctx, cancel := context.WithTimeout(ctx, time.Minute)
defer cancel()
req, err := http.NewRequestWithContext(ctx, "GET", href, nil)
if err != nil {
return "", err
return nil, "", err
}
imgResp, err := imgClient.Do(req)
resp, err := httpClient.Do(req)
if err != nil {
return "", err
return nil, "", err
}
defer imgResp.Body.Close()
if imgResp.StatusCode != 200 {
return "", fmt.Errorf("img %s returned status code %d", href, imgResp.StatusCode)
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil, "", fmt.Errorf("expected status 200 but got %d %s", resp.StatusCode, resp.Status)
}
r := http.MaxBytesReader(nil, imgResp.Body, maxImageSize)
data, err := ioutil.ReadAll(r)
r := http.MaxBytesReader(nil, resp.Body, maxImageSize)
buf, err := ioutil.ReadAll(r)
if err != nil {
return "", err
return nil, "", err
}
mimeType := http.DetectContentType(data)
mimeType = strings.Replace(mimeType, "text/xml", "image/svg+xml", 1)
enc := base64.StdEncoding.EncodeToString(data)
return fmt.Sprintf("data:%s;base64,%s", mimeType, enc), nil
return buf, resp.Header.Get("Content-Type"), nil
}
func read(href string) (string, error) {
data, err := os.ReadFile(href)
if err != nil {
return "", err
// sniffMimeType sniffs the mime type of href based on its file extension and contents.
func sniffMimeType(href, buf []byte, isRemote bool) string {
p := string(href)
if isRemote {
u, err := url.Parse(p)
if err != nil {
p = ""
} else {
p = u.Path
}
}
mimeType := http.DetectContentType(data)
mimeType = strings.Replace(mimeType, "text/xml", "image/svg+xml", 1)
enc := base64.StdEncoding.EncodeToString(data)
return fmt.Sprintf("data:%s;base64,%s", mimeType, enc), nil
mimeType := mime.TypeByExtension(path.Ext(p))
if mimeType == "" {
mimeType = http.DetectContentType(buf)
}
return mimeType
}

View file

@ -12,10 +12,10 @@ import (
"strings"
"testing"
"oss.terrastruct.com/cmdlog"
"oss.terrastruct.com/xos"
"oss.terrastruct.com/util-go/cmdlog"
"oss.terrastruct.com/util-go/xos"
"oss.terrastruct.com/d2/lib/xmain"
"oss.terrastruct.com/util-go/xmain"
)
//go:embed test_png.png
@ -41,7 +41,7 @@ func TestRegex(t *testing.T) {
for _, href := range append(urls, notURLs...) {
str := fmt.Sprintf(`<image href="%s" />`, href)
matches := imageRe.FindAllStringSubmatch(str, -1)
matches := imageRegex.FindAllStringSubmatch(str, -1)
if len(matches) != 1 {
t.Fatalf("uri regex didn't match %s", str)
}
@ -88,9 +88,9 @@ width="328" height="587" viewBox="-100 -131 328 587"><style type="text/css">
Env: xos.NewEnv(os.Environ()),
}
ms.Log = cmdlog.Log(ms.Env, os.Stderr)
ms.Log = cmdlog.NewTB(ms.Env, t)
imgClient.Transport = roundTripFunc(func(req *http.Request) *http.Response {
httpClient.Transport = roundTripFunc(func(req *http.Request) *http.Response {
respRecorder := httptest.NewRecorder()
switch req.URL.String() {
case svgURL:
@ -104,7 +104,7 @@ width="328" height="587" viewBox="-100 -131 328 587"><style type="text/css">
return respRecorder.Result()
})
out, err := InlineRemote(ctx, ms, []byte(sampleSVG))
out, err := BundleRemote(ctx, ms, []byte(sampleSVG))
if err != nil {
t.Fatal(err)
}
@ -119,7 +119,7 @@ width="328" height="587" viewBox="-100 -131 328 587"><style type="text/css">
}
// Test almost too large response
imgClient.Transport = roundTripFunc(func(req *http.Request) *http.Response {
httpClient.Transport = roundTripFunc(func(req *http.Request) *http.Response {
respRecorder := httptest.NewRecorder()
bytes := make([]byte, maxImageSize)
rand.Read(bytes)
@ -127,13 +127,13 @@ width="328" height="587" viewBox="-100 -131 328 587"><style type="text/css">
respRecorder.WriteHeader(200)
return respRecorder.Result()
})
_, err = InlineRemote(ctx, ms, []byte(sampleSVG))
_, err = BundleRemote(ctx, ms, []byte(sampleSVG))
if err != nil {
t.Fatal(err)
}
// Test too large response
imgClient.Transport = roundTripFunc(func(req *http.Request) *http.Response {
httpClient.Transport = roundTripFunc(func(req *http.Request) *http.Response {
respRecorder := httptest.NewRecorder()
bytes := make([]byte, maxImageSize+1)
rand.Read(bytes)
@ -141,18 +141,18 @@ width="328" height="587" viewBox="-100 -131 328 587"><style type="text/css">
respRecorder.WriteHeader(200)
return respRecorder.Result()
})
_, err = InlineRemote(ctx, ms, []byte(sampleSVG))
_, err = BundleRemote(ctx, ms, []byte(sampleSVG))
if err == nil {
t.Fatal("expected error")
}
// Test error response
imgClient.Transport = roundTripFunc(func(req *http.Request) *http.Response {
httpClient.Transport = roundTripFunc(func(req *http.Request) *http.Response {
respRecorder := httptest.NewRecorder()
respRecorder.WriteHeader(500)
return respRecorder.Result()
})
_, err = InlineRemote(ctx, ms, []byte(sampleSVG))
_, err = BundleRemote(ctx, ms, []byte(sampleSVG))
if err == nil {
t.Fatal("expected error")
}
@ -204,8 +204,8 @@ width="328" height="587" viewBox="-100 -131 328 587"><style type="text/css">
Env: xos.NewEnv(os.Environ()),
}
ms.Log = cmdlog.Log(ms.Env, os.Stderr)
out, err := InlineLocal(ctx, ms, []byte(sampleSVG))
ms.Log = cmdlog.NewTB(ms.Env, t)
out, err := BundleLocal(ctx, ms, []byte(sampleSVG))
if err != nil {
t.Fatal(err)
}

View file

@ -9,7 +9,7 @@ import (
"github.com/playwright-community/playwright-go"
"oss.terrastruct.com/d2/lib/xmain"
"oss.terrastruct.com/util-go/xmain"
)
type Playwright struct {

View file

@ -10,8 +10,9 @@ import (
goldmarkHtml "github.com/yuin/goldmark/renderer/html"
"golang.org/x/net/html"
"oss.terrastruct.com/util-go/go2"
"oss.terrastruct.com/d2/d2renderers/d2fonts"
"oss.terrastruct.com/d2/lib/go2"
)
var markdownRenderer goldmark.Markdown

View file

@ -8,7 +8,7 @@ import (
"github.com/stretchr/testify/assert"
"oss.terrastruct.com/d2/d2renderers/d2fonts"
"oss.terrastruct.com/d2/d2renderers/textmeasure"
"oss.terrastruct.com/d2/lib/textmeasure"
)
var txts = []string{

View file

@ -1,4 +1,4 @@
package compress
package urlenc
import (
"bytes"
@ -7,6 +7,8 @@ import (
"io"
"strings"
"oss.terrastruct.com/util-go/xdefer"
"oss.terrastruct.com/d2/d2graph"
)
@ -15,13 +17,25 @@ var compressionDict = "->" +
"--" +
"<->"
var compressionDictBytes []byte
func init() {
for k := range d2graph.StyleKeywords {
compressionDict += k
}
for k := range d2graph.ReservedKeywords {
compressionDict += k
}
for k := range d2graph.ReservedKeywordHolders {
compressionDict += k
}
}
// Compress takes a D2 script and compresses it to a URL-encoded string
func Compress(raw string) (string, error) {
var b bytes.Buffer
// Encode takes a D2 script and encodes it as a compressed base64 string for embedding in URLs.
func Encode(raw string) (_ string, err error) {
defer xdefer.Errorf(&err, "failed to encode d2 script")
zw, err := flate.NewWriterDict(&b, flate.DefaultCompression, []byte(compressionDict))
b := &bytes.Buffer{}
zw, err := flate.NewWriterDict(b, flate.DefaultCompression, []byte(compressionDict))
if err != nil {
return "", err
}
@ -36,8 +50,10 @@ func Compress(raw string) (string, error) {
return encoded, nil
}
// Decompress takes a compressed, URL-encoded string and returns the decompressed D2 script
func Decompress(encoded string) (string, error) {
// Decode decodes a compressed base64 D2 string.
func Decode(encoded string) (_ string, err error) {
defer xdefer.Errorf(&err, "failed to decode d2 script")
b64Decoded, err := base64.URLEncoding.DecodeString(encoded)
if err != nil {
return "", err
@ -53,15 +69,3 @@ func Decompress(encoded string) (string, error) {
}
return b.String(), nil
}
func init() {
for k := range d2graph.StyleKeywords {
compressionDict += k
}
for k := range d2graph.ReservedKeywords {
compressionDict += k
}
for k := range d2graph.ReservedKeywordHolders {
compressionDict += k
}
}

23
lib/urlenc/urlenc_test.go Normal file
View file

@ -0,0 +1,23 @@
package urlenc
import (
"testing"
"oss.terrastruct.com/util-go/assert"
)
func TestBasic(t *testing.T) {
const script = `x -> y
I just forgot my whole philosophy of life!!!: {
s: TV is chewing gum for the eyes
}
`
encoded, err := Encode(script)
assert.Success(t, err)
decoded, err := Decode(encoded)
assert.Success(t, err)
assert.String(t, script, decoded)
}

View file

@ -1,4 +1,4 @@
package version
// Pre-built binaries will have version set during build time.
var Version = "????"
// Pre-built binaries will have version set correctly during build time.
var Version = "v0.0.13-HEAD"

View file

@ -1,25 +0,0 @@
package xbrowser
import (
"context"
"fmt"
"os/exec"
"github.com/pkg/browser"
"oss.terrastruct.com/xos"
)
func OpenURL(ctx context.Context, env *xos.Env, url string) error {
browserEnv := env.Getenv("BROWSER")
if browserEnv != "" {
browserSh := fmt.Sprintf("%s '$1'", browserEnv)
cmd := exec.CommandContext(ctx, "sh", "-c", browserSh, "--", url)
out, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("failed to run %v (out: %q): %w", cmd.Args, out, err)
}
return nil
}
return browser.OpenURL(url)
}

View file

@ -1,57 +0,0 @@
package xexec
import (
"errors"
"io/fs"
"os"
"path/filepath"
"strings"
)
// findExecutable is from package exec
func findExecutable(file string) error {
d, err := os.Stat(file)
if err != nil {
return err
}
if m := d.Mode(); !m.IsDir() && m&0111 != 0 {
return nil
}
return fs.ErrPermission
}
// SearchPath searches for all executables that have prefix in their names in
// the directories named by the PATH environment variable.
func SearchPath(prefix string) ([]string, error) {
var matches []string
envPath := os.Getenv("PATH")
dirSet := make(map[string]struct{})
for _, dir := range filepath.SplitList(envPath) {
if dir == "" {
// From exec package:
// Unix shell semantics: path element "" means "."
dir = "."
}
if _, ok := dirSet[dir]; ok {
continue
}
dirSet[dir] = struct{}{}
files, err := os.ReadDir(dir)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
continue
}
return nil, err
}
for _, f := range files {
if strings.HasPrefix(f.Name(), prefix) {
match := filepath.Join(dir, f.Name())
if err := findExecutable(match); err == nil {
matches = append(matches, match)
}
}
}
}
return matches, nil
}

View file

@ -1,165 +0,0 @@
package xhttp
import (
"encoding/json"
"errors"
"fmt"
"log"
"net/http"
"oss.terrastruct.com/cmdlog"
)
// Error represents an HTTP error.
// It's exported only for comparison in tests.
type Error struct {
Code int
Resp interface{}
Err error
}
var _ interface {
Is(error) bool
Unwrap() error
} = Error{}
// Errorf creates a new error with code, resp, msg and v.
//
// When returned from an xhttp.HandlerFunc, it will be correctly logged
// and written to the connection. See xhttp.WrapHandlerFunc
func Errorf(code int, resp interface{}, msg string, v ...interface{}) error {
return errorWrap(code, resp, fmt.Errorf(msg, v...))
}
// ErrorWrap wraps err with the code and resp for xhttp.HandlerFunc.
//
// When returned from an xhttp.HandlerFunc, it will be correctly logged
// and written to the connection. See xhttp.WrapHandlerFunc
func ErrorWrap(code int, resp interface{}, err error) error {
return errorWrap(code, resp, err)
}
func errorWrap(code int, resp interface{}, err error) error {
if resp == nil {
resp = http.StatusText(code)
}
return Error{code, resp, err}
}
func (e Error) Unwrap() error {
return e.Err
}
func (e Error) Is(err error) bool {
e2, ok := err.(Error)
if !ok {
return false
}
return e.Code == e2.Code && e.Resp == e2.Resp && errors.Is(e.Err, e2.Err)
}
func (e Error) Error() string {
return fmt.Sprintf("http error with code %v and resp %#v: %v", e.Code, e.Resp, e.Err)
}
// HandlerFunc is like http.HandlerFunc but returns an error.
// See Errorf and ErrorWrap.
type HandlerFunc func(w http.ResponseWriter, r *http.Request) error
type HandlerFuncAdapter struct {
Log *cmdlog.Logger
Func HandlerFunc
}
// ServeHTTP adapts xhttp.HandlerFunc into http.Handler for usage with standard
// HTTP routers like chi.
//
// It logs and writes any error from xhttp.HandlerFunc to the connection.
//
// If err was created with xhttp.Errorf or wrapped with xhttp.WrapError, then the error
// will be logged at the correct level for the status code and xhttp.JSON will be called
// with the code and resp.
//
// 400s are logged as warns and 500s as errors.
//
// If the error was not created with the xhttp helpers then a 500 will be written.
//
// If resp is nil, then resp is set to http.StatusText(code)
//
// If the code is not a 400 or a 500, then an error about about the unexpected error code
// will be logged and a 500 will be written. The original error will also be logged.
func (a HandlerFuncAdapter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
var h http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
err := a.Func(w, r)
if err != nil {
handleError(a.Log, w, err)
}
})
h.ServeHTTP(w, r)
}
func handleError(clog *cmdlog.Logger, w http.ResponseWriter, err error) {
var herr Error
ok := errors.As(err, &herr)
if !ok {
herr = ErrorWrap(http.StatusInternalServerError, nil, err).(Error)
}
var logger *log.Logger
switch {
case 400 <= herr.Code && herr.Code < 500:
logger = clog.Warn
case 500 <= herr.Code && herr.Code < 600:
logger = clog.Error
default:
logger = clog.Error
clog.Error.Printf("unexpected non error http status code %d with resp: %#v", herr.Code, herr.Resp)
herr.Code = http.StatusInternalServerError
herr.Resp = nil
}
if herr.Resp == nil {
herr.Resp = http.StatusText(herr.Code)
}
logger.Printf("error handling http request: %v", err)
ww, ok := w.(writtenResponseWriter)
if !ok {
clog.Warn.Printf("response writer does not implement Written, double write logs possible: %#v", w)
} else if ww.Written() {
// Avoid double writes if an error occurred while the response was
// being written.
return
}
JSON(clog, w, herr.Code, map[string]interface{}{
"error": herr.Resp,
})
}
type writtenResponseWriter interface {
Written() bool
}
func JSON(clog *cmdlog.Logger, w http.ResponseWriter, code int, v interface{}) {
if v == nil {
v = map[string]interface{}{
"status": http.StatusText(code),
}
}
b, err := json.Marshal(v)
if err != nil {
clog.Error.Printf("json marshal error: %v", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(code)
_, _ = w.Write(b)
}

View file

@ -1,125 +0,0 @@
package xhttp
import (
"bufio"
"errors"
"fmt"
"log"
"net"
"net/http"
"runtime/debug"
"time"
"golang.org/x/text/message"
"oss.terrastruct.com/cmdlog"
)
type ResponseWriter interface {
http.ResponseWriter
http.Hijacker
http.Flusher
writtenResponseWriter
}
var _ ResponseWriter = &responseWriter{}
type responseWriter struct {
rw http.ResponseWriter
written bool
status int
length int
}
func (rw *responseWriter) Header() http.Header {
return rw.rw.Header()
}
func (rw *responseWriter) WriteHeader(statusCode int) {
if !rw.written {
rw.written = true
rw.status = statusCode
}
rw.rw.WriteHeader(statusCode)
}
func (rw *responseWriter) Write(p []byte) (int, error) {
if !rw.written && len(p) > 0 {
rw.written = true
if rw.status == 0 {
rw.status = http.StatusOK
}
}
rw.length += len(p)
return rw.rw.Write(p)
}
func (rw *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
hj, ok := rw.rw.(http.Hijacker)
if !ok {
return nil, nil, fmt.Errorf("underlying response writer does not implement http.Hijacker: %T", rw.rw)
}
return hj.Hijack()
}
func (rw *responseWriter) Flush() {
f, ok := rw.rw.(http.Flusher)
if !ok {
return
}
f.Flush()
}
func (rw *responseWriter) Written() bool {
return rw.written
}
func Log(clog *cmdlog.Logger, next http.Handler) http.Handler {
englishPrinter := message.NewPrinter(message.MatchLanguage("en"))
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
rec := recover()
if rec != nil {
clog.Error.Printf("caught panic: %#v\n%s", rec, debug.Stack())
JSON(clog, w, http.StatusInternalServerError, map[string]interface{}{
"error": http.StatusText(http.StatusInternalServerError),
})
}
}()
rw := &responseWriter{
rw: w,
}
start := time.Now()
next.ServeHTTP(rw, r)
dur := time.Since(start)
if !rw.Written() {
_, err := rw.Write(nil)
if errors.Is(err, http.ErrHijacked) {
clog.Success.Printf("%s %s %v: hijacked", r.Method, r.URL, dur)
return
}
clog.Warn.Printf("%s %s %v: no response written", r.Method, r.URL, dur)
return
}
var statusLogger *log.Logger
switch {
case 100 <= rw.status && rw.status <= 299:
statusLogger = clog.Success
case 300 <= rw.status && rw.status <= 399:
statusLogger = clog.Info
case 400 <= rw.status && rw.status <= 499:
statusLogger = clog.Warn
case 500 <= rw.status && rw.status <= 599:
statusLogger = clog.Error
}
lengthStr := englishPrinter.Sprint(rw.length)
// TODO: make work with watch.go on hijack, not after
statusLogger.Printf("%s %s %d %sB %v", r.Method, r.URL, rw.status, lengthStr, dur)
})
}

View file

@ -1,44 +0,0 @@
// Package xhttp implements http helpers.
package xhttp
import (
"context"
"log"
"net"
"net/http"
"time"
"oss.terrastruct.com/xcontext"
)
func NewServer(log *log.Logger, h http.Handler) *http.Server {
return &http.Server{
MaxHeaderBytes: 1 << 18, // 262,144B
ReadTimeout: time.Minute,
WriteTimeout: time.Minute,
IdleTimeout: time.Hour,
ErrorLog: log,
Handler: http.MaxBytesHandler(h, 1<<20), // 1,048,576B
}
}
func Serve(ctx context.Context, shutdownTimeout time.Duration, s *http.Server, l net.Listener) error {
s.BaseContext = func(net.Listener) context.Context {
return ctx
}
done := make(chan error, 1)
go func() {
done <- s.Serve(l)
}()
select {
case err := <-done:
return err
case <-ctx.Done():
ctx = xcontext.WithoutCancel(ctx)
ctx, cancel := context.WithTimeout(ctx, shutdownTimeout)
defer cancel()
return s.Shutdown(ctx)
}
}

View file

@ -1,45 +0,0 @@
// flag_helpers.go are private functions from pflag/flag.go
package xmain
import "strings"
func wrap(i, w int, s string) string {
if w == 0 {
return strings.Replace(s, "\n", "\n"+strings.Repeat(" ", i), -1)
}
wrap := w - i
var r, l string
if wrap < 24 {
i = 16
wrap = w - i
r += "\n" + strings.Repeat(" ", i)
}
if wrap < 24 {
return strings.Replace(s, "\n", r, -1)
}
slop := 5
wrap = wrap - slop
l, s = wrapN(wrap, slop, s)
r = r + strings.Replace(l, "\n", "\n"+strings.Repeat(" ", i), -1)
for s != "" {
var t string
t, s = wrapN(wrap, slop, s)
r = r + "\n" + strings.Repeat(" ", i) + strings.Replace(t, "\n", "\n"+strings.Repeat(" ", i), -1)
}
return r
}
func wrapN(i, slop int, s string) (string, string) {
if i+slop > len(s) {
return s, ""
}
w := strings.LastIndexAny(s[:i], " \t\n")
if w <= 0 {
return s, ""
}
nlPos := strings.LastIndex(s[:i], "\n")
if nlPos > 0 && nlPos < w {
return s[:nlPos], s[nlPos+1:]
}
return s[:w], s[w+1:]
}

View file

@ -1,173 +0,0 @@
package xmain
import (
"bytes"
"fmt"
"io"
"strconv"
"strings"
"github.com/spf13/pflag"
"oss.terrastruct.com/cmdlog"
"oss.terrastruct.com/xos"
)
type Opts struct {
Args []string
Flags *pflag.FlagSet
env *xos.Env
log *cmdlog.Logger
flagEnv map[string]string
}
func NewOpts(env *xos.Env, log *cmdlog.Logger, args []string) *Opts {
flags := pflag.NewFlagSet("", pflag.ContinueOnError)
flags.SortFlags = false
flags.Usage = func() {}
flags.SetOutput(io.Discard)
return &Opts{
Args: args,
Flags: flags,
env: env,
log: log,
flagEnv: make(map[string]string),
}
}
// Mostly copy pasted pasted from pflag.FlagUsagesWrapped
// with modifications for env var
func (o *Opts) Defaults() string {
buf := new(bytes.Buffer)
var lines []string
maxlen := 0
maxEnvLen := 0
o.Flags.VisitAll(func(flag *pflag.Flag) {
if flag.Hidden {
return
}
line := ""
if flag.Shorthand != "" && flag.ShorthandDeprecated == "" {
line = fmt.Sprintf(" -%s, --%s", flag.Shorthand, flag.Name)
} else {
line = fmt.Sprintf(" --%s", flag.Name)
}
varname, usage := pflag.UnquoteUsage(flag)
if varname != "" {
line += " " + varname
}
if flag.NoOptDefVal != "" {
switch flag.Value.Type() {
case "string":
line += fmt.Sprintf("[=\"%s\"]", flag.NoOptDefVal)
case "bool":
if flag.NoOptDefVal != "true" {
line += fmt.Sprintf("[=%s]", flag.NoOptDefVal)
}
case "count":
if flag.NoOptDefVal != "+1" {
line += fmt.Sprintf("[=%s]", flag.NoOptDefVal)
}
default:
line += fmt.Sprintf("[=%s]", flag.NoOptDefVal)
}
}
line += "\x00"
if len(line) > maxlen {
maxlen = len(line)
}
if e, ok := o.flagEnv[flag.Name]; ok {
line += fmt.Sprintf("$%s", e)
}
line += "\x01"
if len(line) > maxEnvLen {
maxEnvLen = len(line)
}
line += usage
if flag.Value.Type() == "string" {
line += fmt.Sprintf(" (default %q)", flag.DefValue)
} else {
line += fmt.Sprintf(" (default %s)", flag.DefValue)
}
if len(flag.Deprecated) != 0 {
line += fmt.Sprintf(" (DEPRECATED: %s)", flag.Deprecated)
}
lines = append(lines, line)
})
for _, line := range lines {
sidx1 := strings.Index(line, "\x00")
sidx2 := strings.Index(line, "\x01")
spacing1 := strings.Repeat(" ", maxlen-sidx1)
spacing2 := strings.Repeat(" ", (maxEnvLen-maxlen)-sidx2+sidx1)
fmt.Fprintln(buf, line[:sidx1], spacing1, line[sidx1+1:sidx2], spacing2, wrap(maxEnvLen+3, 0, line[sidx2+1:]))
}
return buf.String()
}
func (o *Opts) getEnv(flag, k string) string {
if k != "" {
o.flagEnv[flag] = k
return o.env.Getenv(k)
}
return ""
}
func (o *Opts) Int64(envKey, flag, shortFlag string, defaultVal int64, usage string) (*int64, error) {
if env := o.getEnv(flag, envKey); env != "" {
envVal, err := strconv.ParseInt(env, 10, 64)
if err != nil {
return nil, fmt.Errorf(`invalid environment variable %s. Expected int64. Found "%v".`, envKey, envVal)
}
defaultVal = envVal
}
return o.Flags.Int64P(flag, shortFlag, defaultVal, usage), nil
}
func (o *Opts) String(envKey, flag, shortFlag string, defaultVal, usage string) *string {
if env := o.getEnv(flag, envKey); env != "" {
defaultVal = env
}
return o.Flags.StringP(flag, shortFlag, defaultVal, usage)
}
func (o *Opts) Bool(envKey, flag, shortFlag string, defaultVal bool, usage string) (*bool, error) {
if env := o.getEnv(flag, envKey); env != "" {
if !boolyEnv(env) {
return nil, fmt.Errorf(`invalid environment variable %s. Expected bool. Found "%s".`, envKey, env)
}
if truthyEnv(env) {
defaultVal = true
} else {
defaultVal = false
}
}
return o.Flags.BoolP(flag, shortFlag, defaultVal, usage), nil
}
func boolyEnv(s string) bool {
return falseyEnv(s) || truthyEnv(s)
}
func falseyEnv(s string) bool {
return s == "0" || s == "false"
}
func truthyEnv(s string) bool {
return s == "1" || s == "true"
}

View file

@ -1,180 +0,0 @@
// Package xmain provides a standard stub for the main of a command handling logging,
// flags, signals and shutdown.
package xmain
import (
"context"
"errors"
"fmt"
"io"
"os"
"os/signal"
"syscall"
"time"
"cdr.dev/slog"
"cdr.dev/slog/sloggers/sloghuman"
"oss.terrastruct.com/xos"
"oss.terrastruct.com/cmdlog"
ctxlog "oss.terrastruct.com/d2/lib/log"
)
type RunFunc func(context.Context, *State) error
func Main(run RunFunc) {
name := ""
args := []string(nil)
if len(os.Args) > 0 {
name = os.Args[0]
args = os.Args[1:]
}
ms := &State{
Name: name,
Stdin: os.Stdin,
Stdout: os.Stdout,
Stderr: os.Stderr,
Env: xos.NewEnv(os.Environ()),
}
ms.Log = cmdlog.Log(ms.Env, os.Stderr)
ms.Opts = NewOpts(ms.Env, ms.Log, args)
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, os.Interrupt, syscall.SIGTERM)
err := ms.Main(context.Background(), sigs, run)
if err != nil {
code := 1
msg := ""
usage := false
var eerr ExitError
var uerr UsageError
if errors.As(err, &eerr) {
code = eerr.Code
msg = eerr.Message
} else if errors.As(err, &uerr) {
msg = err.Error()
usage = true
} else {
msg = err.Error()
}
if msg != "" {
if usage {
msg = fmt.Sprintf("%s\n%s", msg, "Run with --help to see usage.")
}
ms.Log.Error.Print(msg)
}
os.Exit(code)
}
}
type State struct {
Name string
Stdin io.Reader
Stdout io.WriteCloser
Stderr io.WriteCloser
Log *cmdlog.Logger
Env *xos.Env
Opts *Opts
}
func (ms *State) Main(ctx context.Context, sigs <-chan os.Signal, run func(context.Context, *State) error) error {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
done := make(chan error, 1)
go func() {
defer close(done)
done <- run(ctx, ms)
}()
select {
case err := <-done:
return err
case sig := <-sigs:
ms.Log.Warn.Printf("received signal %v: shutting down...", sig)
cancel()
select {
case err := <-done:
if err != nil && !errors.Is(err, context.Canceled) {
return fmt.Errorf("failed to shutdown: %w", err)
}
if sig == syscall.SIGTERM {
// We successfully shutdown.
return nil
}
return ExitError{Code: 1}
case <-time.After(time.Minute):
return ExitError{
Code: 1,
Message: "took longer than 1 minute to shutdown: exiting forcefully",
}
}
}
}
type ExitError struct {
Code int `json:"code"`
Message string `json:"message"`
}
func ExitErrorf(code int, msg string, v ...interface{}) ExitError {
return ExitError{
Code: code,
Message: fmt.Sprintf(msg, v...),
}
}
func (ee ExitError) Error() string {
s := fmt.Sprintf("exiting with code %d", ee.Code)
if ee.Message != "" {
s += ": " + ee.Message
}
return s
}
type UsageError struct {
Message string `json:"message"`
}
func UsageErrorf(msg string, v ...interface{}) UsageError {
return UsageError{
Message: fmt.Sprintf(msg, v...),
}
}
func (ue UsageError) Error() string {
return fmt.Sprintf("bad usage: %s", ue.Message)
}
func (ms *State) ReadPath(fp string) ([]byte, error) {
if fp == "-" {
return io.ReadAll(ms.Stdin)
}
return os.ReadFile(fp)
}
func (ms *State) WritePath(fp string, p []byte) error {
if fp == "-" {
_, err := ms.Stdout.Write(p)
if err != nil {
return err
}
return ms.Stdout.Close()
}
return os.WriteFile(fp, p, 0644)
}
// TODO: remove after removing slog
func DiscardSlog(ctx context.Context) context.Context {
return ctxlog.With(ctx, slog.Make(sloghuman.Sink(io.Discard)))
}

View file

@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"io"
"os/exec"
"path/filepath"
"strings"
@ -11,17 +12,23 @@ import (
"github.com/playwright-community/playwright-go"
"github.com/spf13/pflag"
"go.uber.org/multierr"
"oss.terrastruct.com/d2"
"oss.terrastruct.com/util-go/xmain"
"oss.terrastruct.com/d2/d2lib"
"oss.terrastruct.com/d2/d2plugin"
"oss.terrastruct.com/d2/d2renderers/d2svg"
"oss.terrastruct.com/d2/d2renderers/textmeasure"
"oss.terrastruct.com/d2/d2themes"
"oss.terrastruct.com/d2/d2themes/d2themescatalog"
"oss.terrastruct.com/d2/lib/imgbundler"
ctxlog "oss.terrastruct.com/d2/lib/log"
"oss.terrastruct.com/d2/lib/png"
"oss.terrastruct.com/d2/lib/textmeasure"
"oss.terrastruct.com/d2/lib/version"
"oss.terrastruct.com/d2/lib/xmain"
"cdr.dev/slog"
"cdr.dev/slog/sloggers/sloghuman"
)
func main() {
@ -30,31 +37,31 @@ func main() {
func run(ctx context.Context, ms *xmain.State) (err error) {
// :(
ctx = xmain.DiscardSlog(ctx)
ctx = DiscardSlog(ctx)
// These should be kept up-to-date with the d2 man page
watchFlag, err := ms.Opts.Bool("D2_WATCH", "watch", "w", false, "watch for changes to input and live reload. Use $HOST and $PORT to specify the listening address.\n(default localhost:0, which is will open on a randomly available local port).")
if err != nil {
return xmain.UsageErrorf(err.Error())
return err
}
hostFlag := ms.Opts.String("HOST", "host", "h", "localhost", "host listening address when used with watch")
portFlag := ms.Opts.String("PORT", "port", "p", "0", "port listening address when used with watch")
bundleFlag, err := ms.Opts.Bool("D2_BUNDLE", "bundle", "b", true, "when outputting SVG, bundle all assets and layers into the output file.")
if err != nil {
return xmain.UsageErrorf(err.Error())
return err
}
debugFlag, err := ms.Opts.Bool("DEBUG", "debug", "d", false, "print debug logs.")
if err != nil {
return xmain.UsageErrorf(err.Error())
return err
}
layoutFlag := ms.Opts.String("D2_LAYOUT", "layout", "l", "dagre", `the layout engine used.`)
themeFlag, err := ms.Opts.Int64("D2_THEME", "theme", "t", 0, "the diagram theme ID. For a list of available options, see https://oss.terrastruct.com/d2")
if err != nil {
return xmain.UsageErrorf(err.Error())
return err
}
versionFlag, err := ms.Opts.Bool("", "version", "v", false, "get the version")
if err != nil {
return xmain.UsageErrorf(err.Error())
return err
}
err = ms.Opts.Flags.Parse(ms.Opts.Args)
@ -62,18 +69,26 @@ func run(ctx context.Context, ms *xmain.State) (err error) {
return xmain.UsageErrorf("failed to parse flags: %v", err)
}
if len(ms.Opts.Flags.Args()) > 0 {
switch ms.Opts.Flags.Arg(0) {
case "layout":
return layoutHelp(ctx, ms)
}
}
if errors.Is(err, pflag.ErrHelp) {
help(ms)
return nil
}
if len(ms.Opts.Flags.Args()) > 0 {
switch ms.Opts.Flags.Arg(0) {
case "layout":
return layoutCmd(ctx, ms)
case "fmt":
return fmtCmd(ctx, ms)
case "version":
if len(ms.Opts.Flags.Args()) > 1 {
return xmain.UsageErrorf("version subcommand accepts no arguments")
}
fmt.Println(version.Version)
return nil
}
}
if *debugFlag {
ms.Env.Setenv("DEBUG", "1")
}
@ -93,10 +108,6 @@ func run(ctx context.Context, ms *xmain.State) (err error) {
}
if len(ms.Opts.Flags.Args()) >= 1 {
if ms.Opts.Flags.Arg(0) == "version" {
fmt.Println(version.Version)
return nil
}
inputPath = ms.Opts.Flags.Arg(0)
}
if len(ms.Opts.Flags.Args()) >= 2 {
@ -146,7 +157,7 @@ func run(ctx context.Context, ms *xmain.State) (err error) {
if inputPath == "-" {
return xmain.UsageErrorf("-w[atch] cannot be combined with reading input from stdin")
}
ms.Env.Setenv("LOG_TIMESTAMPS", "1")
ms.Log.SetTS(true)
w, err := newWatcher(ctx, ms, watcherOpts{
layoutPlugin: plugin,
themeID: *themeFlag,
@ -154,6 +165,7 @@ func run(ctx context.Context, ms *xmain.State) (err error) {
port: *portFlag,
inputPath: inputPath,
outputPath: outputPath,
bundle: *bundleFlag,
pw: pw,
})
if err != nil {
@ -165,77 +177,75 @@ func run(ctx context.Context, ms *xmain.State) (err error) {
ctx, cancel := context.WithTimeout(ctx, time.Minute*2)
defer cancel()
if *bundleFlag {
_ = 343
}
_, err = compile(ctx, ms, false, plugin, *themeFlag, inputPath, outputPath, pw.Page)
_, written, err := compile(ctx, ms, plugin, *themeFlag, inputPath, outputPath, *bundleFlag, pw.Page)
if err != nil {
return err
if written {
return fmt.Errorf("failed to fully compile (partial render written): %w", err)
}
return fmt.Errorf("failed to compile: %w", err)
}
ms.Log.Success.Printf("successfully compiled %v to %v", inputPath, outputPath)
return nil
}
func compile(ctx context.Context, ms *xmain.State, isWatching bool, plugin d2plugin.Plugin, themeID int64, inputPath, outputPath string, page playwright.Page) ([]byte, error) {
func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, themeID int64, inputPath, outputPath string, bundle bool, page playwright.Page) (_ []byte, written bool, _ error) {
input, err := ms.ReadPath(inputPath)
if err != nil {
return nil, err
return nil, false, err
}
ruler, err := textmeasure.NewRuler()
if err != nil {
return nil, err
return nil, false, err
}
layout := plugin.Layout
d, err := d2.Compile(ctx, string(input), &d2.CompileOptions{
diagram, _, err := d2lib.Compile(ctx, string(input), &d2lib.CompileOptions{
Layout: layout,
Ruler: ruler,
ThemeID: themeID,
})
if err != nil {
return nil, err
return nil, false, err
}
svg, err := d2svg.Render(d)
svg, err := d2svg.Render(diagram)
if err != nil {
return nil, err
return nil, false, err
}
svg, err = plugin.PostProcess(ctx, svg)
if err != nil {
return nil, err
return svg, false, err
}
svg, err = imgbundler.InlineLocal(ctx, ms, svg)
if err != nil {
ms.Log.Error.Printf("missing/broken local image(s), writing partial output: %v", err)
svg, bundleErr := imgbundler.BundleLocal(ctx, ms, svg)
if bundle {
var bundleErr2 error
svg, bundleErr2 = imgbundler.BundleRemote(ctx, ms, svg)
bundleErr = multierr.Combine(bundleErr, bundleErr2)
}
out := svg
if filepath.Ext(outputPath) == ".png" {
svg, err = imgbundler.InlineRemote(ctx, ms, svg)
if err != nil {
ms.Log.Error.Printf("missing/broken remote image(s), writing partial output: %v", err)
svg := svg
if !bundle {
var bundleErr2 error
svg, bundleErr2 = imgbundler.BundleRemote(ctx, ms, svg)
bundleErr = multierr.Combine(bundleErr, bundleErr2)
}
out, err = png.ConvertSVG(ms, page, svg)
if err != nil {
return nil, err
return svg, false, err
}
}
err = ms.WritePath(outputPath, out)
if err != nil {
return nil, err
return svg, false, err
}
// Missing/broken images are fine during watch mode, as the user is likely building up a diagram.
// Otherwise, the assumption is that this diagram is building for production, and broken images are not okay.
if !isWatching && ms.Log.Nerrors() > 0 {
return nil, xmain.ExitErrorf(1, "errors logged while rendering, partial output written to %v", outputPath)
}
return svg, nil
return svg, true, bundleErr
}
// newExt must include leading .
@ -247,3 +257,8 @@ func renameExt(fp string, newExt string) string {
return strings.TrimSuffix(fp, ext) + newExt
}
}
// TODO: remove after removing slog
func DiscardSlog(ctx context.Context) context.Context {
return ctxlog.With(ctx, slog.Make(sloghuman.Sink(io.Discard)))
}

View file

@ -1,7 +1,9 @@
#!/bin/sh
set -eu
if [ ! -d "$(dirname "$0")/ci/sub/.git" ]; then
if [ ! -e "$(dirname "$0")/ci/sub/.git" ]; then
set -x
git submodule update --init
set +x
fi
. "$(dirname "$0")/ci/sub/lib.sh"
PATH="$(cd -- "$(dirname "$0")" && pwd)/ci/sub/bin:$PATH"

View file

@ -1,4 +1,4 @@
#d2c-err {
#d2-err {
/* This style was copied from Chrome's svg parser error style. */
white-space: pre-wrap;
border: 2px solid #c77;

View file

@ -4,6 +4,9 @@ window.addEventListener("DOMContentLoaded", () => {
});
function init(reconnectDelay) {
const d2ErrDiv = window.document.querySelector("#d2-err");
const d2SVG = window.document.querySelector("#d2-svg");
const devMode = document.body.dataset.d2DevMode === "true";
const ws = new WebSocket(
`ws://${window.location.host}${window.location.pathname}watch`
@ -19,13 +22,7 @@ function init(reconnectDelay) {
} else {
console.debug("watch websocket received data");
}
const d2ErrDiv = window.document.querySelector("#d2-err");
if (msg.err) {
d2ErrDiv.innerText = msg.err;
d2ErrDiv.style.display = "block";
d2ErrDiv.scrollIntoView();
} else {
const d2SVG = window.document.querySelector("#d2-svg");
if (msg.svg) {
// We could turn d2SVG into an actual SVG element and use outerHTML to fully replace it
// with the result from the renderer but unfortunately that overwrites the #d2-svg ID.
// Even if you add another line to set it afterwards. The parsing/interpretation of outerHTML must be async.
@ -36,6 +33,11 @@ function init(reconnectDelay) {
d2SVG.innerHTML = msg.svg;
d2ErrDiv.style.display = "none";
}
if (msg.err) {
d2ErrDiv.innerText = msg.err;
d2ErrDiv.style.display = "block";
d2ErrDiv.scrollIntoView();
}
};
ws.onerror = (ev) => {
console.error("watch websocket connection error", ev);

View file

@ -19,11 +19,14 @@ import (
"nhooyr.io/websocket"
"nhooyr.io/websocket/wsjson"
"oss.terrastruct.com/util-go/xbrowser"
"oss.terrastruct.com/util-go/xhttp"
"oss.terrastruct.com/util-go/xmain"
"oss.terrastruct.com/d2/d2plugin"
"oss.terrastruct.com/d2/lib/png"
"oss.terrastruct.com/d2/lib/xbrowser"
"oss.terrastruct.com/d2/lib/xhttp"
"oss.terrastruct.com/d2/lib/xmain"
)
// Enabled with the build tag "dev".
@ -42,6 +45,7 @@ type watcherOpts struct {
port string
inputPath string
outputPath string
bundle bool
pw png.Playwright
}
@ -73,8 +77,8 @@ type watcher struct {
}
type compileResult struct {
Err string `json:"err"`
SVG string `json:"svg"`
Err string `json:"err"`
}
func newWatcher(ctx context.Context, ms *xmain.State, opts watcherOpts) (*watcher, error) {
@ -250,7 +254,7 @@ func (w *watcher) watchLoop(ctx context.Context) error {
// We missed changes.
lastModified = mt
}
// The purpose of eatBurstTimer is to wait at least 32 milliseconds after a sequence of
// The purpose of eatBurstTimer is to wait at least 16 milliseconds after a sequence of
// events to ensure that whomever is editing the file is now done.
//
// For example, On macOS editing with neovim, every write I see a chmod immediately
@ -260,7 +264,7 @@ func (w *watcher) watchLoop(ctx context.Context) error {
// Another example would be a very large file where one logical edit becomes write
// events. We wouldn't want to try to compile an incomplete file and then report a
// misleading error.
eatBurstTimer.Reset(time.Millisecond * 32)
eatBurstTimer.Reset(time.Millisecond * 16)
case <-eatBurstTimer.C:
w.ms.Log.Info.Printf("detected change in %v: recompiling...", w.inputPath)
w.requestCompile()
@ -283,7 +287,7 @@ func (w *watcher) requestCompile() {
}
func (w *watcher) ensureAddWatch(ctx context.Context) (time.Time, error) {
interval := time.Second
interval := time.Millisecond * 16
tc := time.NewTimer(0)
<-tc.C
for {
@ -291,11 +295,16 @@ func (w *watcher) ensureAddWatch(ctx context.Context) (time.Time, error) {
if err == nil {
return mt, nil
}
w.ms.Log.Error.Printf("failed to watch inputPath %q: %v (retrying in %v)", w.inputPath, err, interval)
if interval >= time.Second {
w.ms.Log.Error.Printf("failed to watch inputPath %q: %v (retrying in %v)", w.inputPath, err, interval)
}
tc.Reset(interval)
select {
case <-tc.C:
if interval < time.Second {
interval = time.Second
}
if interval < time.Second*16 {
interval *= 2
}
@ -345,24 +354,28 @@ func (w *watcher) compileLoop(ctx context.Context) error {
w.pw = newPW
}
b, err := compile(ctx, w.ms, true, w.layoutPlugin, w.themeID, w.inputPath, w.outputPath, w.pw.Page)
svg, _, err := compile(ctx, w.ms, w.layoutPlugin, w.themeID, w.inputPath, w.outputPath, w.bundle, w.pw.Page)
errs := ""
if err != nil {
err = fmt.Errorf("failed to %scompile: %w", recompiledPrefix, err)
w.ms.Log.Error.Print(err)
w.broadcast(&compileResult{
Err: err.Error(),
})
if len(svg) > 0 {
err = fmt.Errorf("failed to fully %scompile (rendering partial svg): %w", recompiledPrefix, err)
} else {
err = fmt.Errorf("failed to %scompile: %w", recompiledPrefix, err)
}
errs = err.Error()
w.ms.Log.Error.Print(errs)
} else {
w.ms.Log.Success.Printf("successfully %scompiled %v to %v", recompiledPrefix, w.inputPath, w.outputPath)
w.broadcast(&compileResult{
SVG: string(b),
})
}
w.broadcast(&compileResult{
SVG: string(svg),
Err: errs,
})
if firstCompile {
firstCompile = false
url := fmt.Sprintf("http://%s", w.l.Addr())
err = xbrowser.OpenURL(ctx, w.ms.Env, url)
err = xbrowser.Open(ctx, w.ms.Env, url)
if err != nil {
w.ms.Log.Warn.Printf("failed to open browser to %v: %v", url, err)
}