Merge branch 'master' into nested-sequence-diagrams
This commit is contained in:
commit
f9f02015e6
92 changed files with 1049 additions and 1504 deletions
4
.github/workflows/daily.yml
vendored
4
.github/workflows/daily.yml
vendored
|
|
@ -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
15
.github/workflows/project.yml
vendored
Normal 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
1
.gitignore
vendored
|
|
@ -4,3 +4,4 @@
|
|||
*.got.json
|
||||
*.got.svg
|
||||
e2e_report.html
|
||||
bin
|
||||
|
|
|
|||
2
Makefile
2
Makefile
|
|
@ -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
112
README.md
|
|
@ -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)
|
||||
|
||||
[](https://github.com/terrastruct/d2/actions/workflows/ci.yml)
|
||||
[](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
7
ci/dev.sh
Executable 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
12
ci/fmt.sh
Executable 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
|
||||
|
|
@ -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")"
|
||||
|
|
|
|||
|
|
@ -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 "$@"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
#### Features 🚀
|
||||
#### Features 💸
|
||||
|
||||
#### Improvements 🔧
|
||||
#### Improvements 🧹
|
||||
|
||||
#### Bugfixes 🔴
|
||||
#### Bugfixes ⛑️
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 "$@"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
2
ci/sub
|
|
@ -1 +1 @@
|
|||
Subproject commit 28fb67e3bf11d7df2be9ad57b67b78a1733a7f2d
|
||||
Subproject commit 70a9ad95ea0ae1de83fa3b7f7d4a160db4853c20
|
||||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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), "."))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ package d2graph
|
|||
import (
|
||||
"encoding/json"
|
||||
|
||||
"oss.terrastruct.com/d2/lib/go2"
|
||||
"oss.terrastruct.com/util-go/go2"
|
||||
)
|
||||
|
||||
type SerializedGraph struct {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
//go:build cgo
|
||||
|
||||
package d2
|
||||
package d2lib
|
||||
|
||||
import "oss.terrastruct.com/d2/d2layouts/d2dagrelayout"
|
||||
|
||||
|
|
@ -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) {
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import (
|
|||
"os/exec"
|
||||
"time"
|
||||
|
||||
"oss.terrastruct.com/xdefer"
|
||||
"oss.terrastruct.com/util-go/xdefer"
|
||||
|
||||
"oss.terrastruct.com/d2/d2graph"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import (
|
|||
"regexp"
|
||||
"strconv"
|
||||
|
||||
"oss.terrastruct.com/xdefer"
|
||||
"oss.terrastruct.com/util-go/xdefer"
|
||||
v8 "rogchap.com/v8go"
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
1
docs/examples/lib/1-d2lib/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
out.svg
|
||||
25
docs/examples/lib/1-d2lib/d2lib.go
Normal file
25
docs/examples/lib/1-d2lib/d2lib.go
Normal 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)
|
||||
}
|
||||
9
docs/examples/lib/1-d2lib/d2lib_test.go
Normal file
9
docs/examples/lib/1-d2lib/d2lib_test.go
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMain_(t *testing.T) {
|
||||
main()
|
||||
}
|
||||
36
docs/examples/lib/2-d2oracle/d2oracle.go
Normal file
36
docs/examples/lib/2-d2oracle/d2oracle.go
Normal 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))
|
||||
}
|
||||
9
docs/examples/lib/2-d2oracle/d2oracle_test.go
Normal file
9
docs/examples/lib/2-d2oracle/d2oracle_test.go
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMain_(t *testing.T) {
|
||||
main()
|
||||
}
|
||||
1
docs/examples/lib/3-lowlevel/.gitignore
vendored
Normal file
1
docs/examples/lib/3-lowlevel/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
out.svg
|
||||
26
docs/examples/lib/3-lowlevel/lowlevel.go
Normal file
26
docs/examples/lib/3-lowlevel/lowlevel.go
Normal 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)
|
||||
}
|
||||
9
docs/examples/lib/3-lowlevel/lowlevel_test.go
Normal file
9
docs/examples/lib/3-lowlevel/lowlevel_test.go
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMain_(t *testing.T) {
|
||||
main()
|
||||
}
|
||||
30
docs/examples/lib/README.md
Normal file
30
docs/examples/lib/README.md
Normal 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.
|
||||
|
|
@ -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,35 +122,24 @@ 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"))
|
||||
}
|
||||
}
|
||||
|
||||
func getShape(t *testing.T, diagram *d2target.Diagram, id string) d2target.Shape {
|
||||
|
|
|
|||
37
fmt.go
Normal file
37
fmt.go
Normal 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
16
go.mod
generated
|
|
@ -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
16
go.sum
generated
|
|
@ -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=
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
55
install.sh
55
install.sh
|
|
@ -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 "$@"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)))
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
if resp.err != nil {
|
||||
err = multierr.Combine(err, resp.err)
|
||||
continue
|
||||
return svg, nil
|
||||
}
|
||||
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)
|
||||
// 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 {
|
||||
return "", err
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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{
|
||||
|
|
@ -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
23
lib/urlenc/urlenc_test.go
Normal 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)
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
165
lib/xhttp/err.go
165
lib/xhttp/err.go
|
|
@ -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)
|
||||
}
|
||||
125
lib/xhttp/log.go
125
lib/xhttp/log.go
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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:]
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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)))
|
||||
}
|
||||
|
|
@ -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)))
|
||||
}
|
||||
4
make.sh
4
make.sh
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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);
|
||||
|
|
@ -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
|
||||
}
|
||||
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 {
|
||||
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)
|
||||
w.ms.Log.Error.Print(err)
|
||||
w.broadcast(&compileResult{
|
||||
Err: err.Error(),
|
||||
})
|
||||
}
|
||||
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)
|
||||
}
|
||||
Loading…
Reference in a new issue