Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cli: add format flag to support PNG output to stdout #2257

Closed
wants to merge 36 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
b15161f
feat(cli): add format flag to support PNG output to stdout
Maricaya Dec 21, 2024
9bd151b
refactor: rename format flag to stdout-format
Maricaya Dec 30, 2024
5c43324
docs: update lib examples with proper context logging
alixander Dec 27, 2024
674257a
d2sequence: fix notes with self messages
alixander Dec 29, 2024
7a3d54b
ta
alixander Dec 29, 2024
45f978e
next
alixander Dec 29, 2024
2d59fda
d2lsp: implement autocomplete functions
alixander Dec 29, 2024
0b5fd2d
shuffle keywords
alixander Dec 29, 2024
081dd1b
init
alixander Apr 3, 2023
cd56cbe
remove layers restriction
alixander Jun 11, 2023
7840eab
update format
alixander Jun 14, 2023
b43fecf
defer imports
alixander Jun 15, 2023
cdf771a
compile
alixander Jul 22, 2023
6277d6d
fix importUsed
alixander Aug 4, 2023
084a62c
invoke callback if defined
alixander Sep 25, 2023
21afb48
no goldmark in compile
alixander Oct 2, 2023
7dea1db
getparentid
alixander Jan 20, 2024
b3e79f7
add version call
alixander Sep 4, 2024
e75b2e9
update getRefRanges
alixander Oct 11, 2024
2e6234a
update getrefs call
alixander Oct 11, 2024
63ddaeb
support board path
alixander Oct 12, 2024
84edf7e
rebase
alixander Oct 12, 2024
6656679
update lsp
alixander Oct 17, 2024
5c40b01
update to latest
alixander Oct 19, 2024
5cb8a62
get board at position
alixander Nov 13, 2024
ecdf977
add wasm tag
alixander Dec 12, 2024
3db5d79
refactor
alixander Dec 21, 2024
80f1354
integrate with d2lsp completions
alixander Dec 29, 2024
3a6617a
compile and render functions
alixander Dec 29, 2024
688060d
Implement --check flag for fmt
nwalters512 Dec 16, 2024
0642d5c
Add E2E test
nwalters512 Dec 16, 2024
5aab612
Update changelog
nwalters512 Dec 16, 2024
5339983
Fix typo
nwalters512 Dec 16, 2024
63ebe96
Update the manpage
nwalters512 Dec 16, 2024
564cd3a
Improve tests
nwalters512 Dec 17, 2024
714c20e
feat: Update the manpage
Maricaya Dec 30, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions ci/release/changelogs/next.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
- Connections now support `link` [#1955](https://github.com/terrastruct/d2/pull/1955)
- Vars: vars in markdown blocks are substituted [#2218](https://github.com/terrastruct/d2/pull/2218)
- Markdown: Github-flavored tables work in `md` blocks [#2221](https://github.com/terrastruct/d2/pull/2221)
- CLI: PNG output to stdout is supported using `--stdout-format png -` [#2260](https://github.com/terrastruct/d2/pull/2260)
- `d2 fmt` now supports a `--check` flag [#2253](https://github.com/terrastruct/d2/pull/2253)

#### Improvements 🧹

Expand All @@ -20,3 +22,4 @@
- Imports: fixes using substitutions in `icon` values [#2207](https://github.com/terrastruct/d2/pull/2207)
- Markdown: fixes ampersands in URLs in markdown [#2219](https://github.com/terrastruct/d2/pull/2219)
- Globs: fixes edge case where globs with imported boards would create empty boards [#2247](https://github.com/terrastruct/d2/pull/2247)
- Sequence diagrams: fixes alignment of notes when self messages are above it [#2264](https://github.com/terrastruct/d2/pull/2264)
10 changes: 10 additions & 0 deletions ci/release/template/man/d2.1
Original file line number Diff line number Diff line change
Expand Up @@ -125,12 +125,18 @@ In watch mode, images used in icons are cached for subsequent compilations. This
.It Fl -timeout Ar 120
The maximum number of seconds that D2 runs for before timing out and exiting. When rendering a large diagram, it is recommended to increase this value
.Ns .
.It Fl -check Ar false
Check that the specified files are formatted correctly
.Ns .
.It Fl h , -help
Print usage information and exit
.Ns .
.It Fl v , -version
Print version information and exit
.Ns .
.It Fl -stdout-format Ar string
Set the output format when writing to stdout. Supported formats are: png, svg. Only used when output is set to stdout (-)
.Ns .
.El
.Sh SUBCOMMANDS
.Bl -tag -width Fl
Expand Down Expand Up @@ -180,6 +186,8 @@ See --font-semibold flag.
See --animate-interval flag.
.It Ev Sy D2_TIMEOUT
See --timeout flag.
.It Ev Sy D2_CHECK
See --check flag.
.El
.Bl -tag -width Ds
.It Ev Sy DEBUG
Expand All @@ -192,6 +200,8 @@ See -h[ost] flag.
See -p[ort] flag.
.It Ev Sy BROWSER
See --browser flag.
.It Ev Sy D2_STDOUT_FORMAT
See --stdout-format flag.
.El
.Sh SEE ALSO
.Xr d2plugin-tala 1
Expand Down
15 changes: 7 additions & 8 deletions d2ast/keywords.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ var ReservedKeywords map[string]struct{}
// Non Style/Holder keywords.
var SimpleReservedKeywords = map[string]struct{}{
"label": {},
"desc": {},
"shape": {},
"icon": {},
"constraint": {},
Expand All @@ -31,17 +30,17 @@ var SimpleReservedKeywords = map[string]struct{}{

// ReservedKeywordHolders are reserved keywords that are meaningless on its own and must hold composites
var ReservedKeywordHolders = map[string]struct{}{
"style": {},
"source-arrowhead": {},
"target-arrowhead": {},
"style": {},
}

// CompositeReservedKeywords are reserved keywords that can hold composites
var CompositeReservedKeywords = map[string]struct{}{
"classes": {},
"constraint": {},
"label": {},
"icon": {},
"source-arrowhead": {},
"target-arrowhead": {},
"classes": {},
"constraint": {},
"label": {},
"icon": {},
}

// StyleKeywords are reserved keywords which cannot exist outside of the "style" keyword
Expand Down
20 changes: 20 additions & 0 deletions d2cli/export.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package d2cli

import (
"fmt"
"path/filepath"
"strings"
)

type exportExtension string
Expand All @@ -14,6 +16,24 @@ const SVG exportExtension = ".svg"

var SUPPORTED_EXTENSIONS = []exportExtension{SVG, PNG, PDF, PPTX, GIF}

var STDOUT_FORMAT_MAP = map[string]exportExtension{
"png": PNG,
"svg": SVG,
}

var SUPPORTED_STDOUT_FORMATS = []string{"png", "svg"}

func getOutputFormat(stdoutFormatFlag *string, outputPath string) (exportExtension, error) {
if *stdoutFormatFlag != "" {
format := strings.ToLower(*stdoutFormatFlag)
if ext, ok := STDOUT_FORMAT_MAP[format]; ok {
return ext, nil
}
return "", fmt.Errorf("%s is not a supported format. Supported formats are: %s", *stdoutFormatFlag, SUPPORTED_STDOUT_FORMATS)
}
return getExportExtension(outputPath), nil
}

func getExportExtension(outputPath string) exportExtension {
ext := filepath.Ext(outputPath)
for _, kext := range SUPPORTED_EXTENSIONS {
Expand Down
13 changes: 12 additions & 1 deletion d2cli/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

func TestOutputFormat(t *testing.T) {
type testCase struct {
stdoutFormatFlag string
outputPath string
extension exportExtension
supportsDarkTheme bool
Expand Down Expand Up @@ -41,6 +42,15 @@ func TestOutputFormat(t *testing.T) {
requiresAnimationInterval: false,
requiresPngRender: false,
},
{
stdoutFormatFlag: "png",
outputPath: "-",
extension: PNG,
supportsDarkTheme: false,
supportsAnimation: false,
requiresAnimationInterval: false,
requiresPngRender: true,
},
{
outputPath: "/out.png",
extension: PNG,
Expand Down Expand Up @@ -78,7 +88,8 @@ func TestOutputFormat(t *testing.T) {
for _, tc := range testCases {
tc := tc
t.Run(tc.outputPath, func(t *testing.T) {
extension := getExportExtension(tc.outputPath)
extension, err := getOutputFormat(&tc.stdoutFormatFlag, tc.outputPath)
assert.NoError(t, err)
assert.Equal(t, tc.extension, extension)
assert.Equal(t, tc.supportsAnimation, extension.supportsAnimation())
assert.Equal(t, tc.supportsDarkTheme, extension.supportsDarkTheme())
Expand Down
24 changes: 21 additions & 3 deletions d2cli/fmt.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,19 @@ import (

"oss.terrastruct.com/d2/d2format"
"oss.terrastruct.com/d2/d2parser"
"oss.terrastruct.com/d2/lib/log"
)

func fmtCmd(ctx context.Context, ms *xmain.State) (err error) {
func fmtCmd(ctx context.Context, ms *xmain.State, check bool) (err error) {
defer xdefer.Errorf(&err, "failed to fmt")

ms.Opts = xmain.NewOpts(ms.Env, ms.Opts.Flags.Args()[1:])
if len(ms.Opts.Args) == 0 {
return xmain.UsageErrorf("fmt must be passed at least one file to be formatted")
}

unformattedCount := 0

for _, inputPath := range ms.Opts.Args {
if inputPath != "-" {
inputPath = ms.AbsPath(inputPath)
Expand All @@ -43,10 +46,25 @@ func fmtCmd(ctx context.Context, ms *xmain.State) (err error) {

output := []byte(d2format.Format(m))
if !bytes.Equal(output, input) {
if err := ms.WritePath(inputPath, output); err != nil {
return err
if check {
unformattedCount += 1
log.Warn(ctx, inputPath)
} else {
if err := ms.WritePath(inputPath, output); err != nil {
return err
}
}
}
}

if unformattedCount > 0 {
pluralFiles := "file"
if unformattedCount > 1 {
pluralFiles = "files"
}

return xmain.ExitErrorf(1, "found %d unformatted %s. Run d2 fmt to fix.", unformattedCount, pluralFiles)
}

return nil
}
48 changes: 32 additions & 16 deletions d2cli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,11 @@ func Run(ctx context.Context, ms *xmain.State) (err error) {
if err != nil {
return err
}
stdoutFormatFlag := ms.Opts.String("", "stdout-format", "", "", "output format when writing to stdout (svg, png). Usage: d2 input.d2 --stdout-format png - > output.png")
if err != nil {
return err
}

browserFlag := ms.Opts.String("BROWSER", "browser", "", "", "browser executable that watch opens. Setting to 0 opens no browser.")
centerFlag, err := ms.Opts.Bool("D2_CENTER", "center", "c", false, "center the SVG in the containing viewbox, such as your browser screen")
if err != nil {
Expand All @@ -119,6 +124,11 @@ func Run(ctx context.Context, ms *xmain.State) (err error) {
fontBoldFlag := ms.Opts.String("D2_FONT_BOLD", "font-bold", "", "", "path to .ttf file to use for the bold font. If none provided, Source Sans Pro Bold is used.")
fontSemiboldFlag := ms.Opts.String("D2_FONT_SEMIBOLD", "font-semibold", "", "", "path to .ttf file to use for the semibold font. If none provided, Source Sans Pro Semibold is used.")

checkFlag, err := ms.Opts.Bool("D2_CHECK", "check", "", false, "check that the specified files are formatted correctly.")
if err != nil {
return err
}

plugins, err := d2plugin.ListPlugins(ctx)
if err != nil {
return err
Expand Down Expand Up @@ -153,7 +163,7 @@ func Run(ctx context.Context, ms *xmain.State) (err error) {
themesCmd(ctx, ms)
return nil
case "fmt":
return fmtCmd(ctx, ms)
return fmtCmd(ctx, ms, *checkFlag)
case "version":
if len(ms.Opts.Flags.Args()) > 1 {
return xmain.UsageErrorf("version subcommand accepts no arguments")
Expand Down Expand Up @@ -213,7 +223,12 @@ func Run(ctx context.Context, ms *xmain.State) (err error) {
if filepath.Ext(outputPath) == ".ppt" {
return xmain.UsageErrorf("D2 does not support ppt exports, did you mean \"pptx\"?")
}
outputFormat := getExportExtension(outputPath)

outputFormat, err := getOutputFormat(stdoutFormatFlag, outputPath)
if err != nil {
return xmain.UsageErrorf("%v", err)
}

if outputPath != "-" {
outputPath = ms.AbsPath(outputPath)
if *animateIntervalFlag > 0 && !outputFormat.supportsAnimation() {
Expand Down Expand Up @@ -325,6 +340,7 @@ func Run(ctx context.Context, ms *xmain.State) (err error) {
forceAppendix: *forceAppendixFlag,
pw: pw,
fontFamily: fontFamily,
outputFormat: outputFormat,
})
if err != nil {
return err
Expand Down Expand Up @@ -355,7 +371,7 @@ func Run(ctx context.Context, ms *xmain.State) (err error) {
ctx, cancel := timelib.WithTimeout(ctx, time.Minute*2)
defer cancel()

_, written, err := compile(ctx, ms, plugins, nil, layoutFlag, renderOpts, fontFamily, *animateIntervalFlag, inputPath, outputPath, boardPath, noChildren, *bundleFlag, *forceAppendixFlag, pw.Page)
_, written, err := compile(ctx, ms, plugins, nil, layoutFlag, renderOpts, fontFamily, *animateIntervalFlag, inputPath, outputPath, boardPath, noChildren, *bundleFlag, *forceAppendixFlag, pw.Page, outputFormat)
if err != nil {
if written {
return fmt.Errorf("failed to fully compile (partial render written) %s: %w", ms.HumanPath(inputPath), err)
Expand Down Expand Up @@ -430,7 +446,7 @@ func RouterResolver(ctx context.Context, ms *xmain.State, plugins []d2plugin.Plu
}
}

func compile(ctx context.Context, ms *xmain.State, plugins []d2plugin.Plugin, fs fs.FS, layout *string, renderOpts d2svg.RenderOpts, fontFamily *d2fonts.FontFamily, animateInterval int64, inputPath, outputPath string, boardPath []string, noChildren, bundle, forceAppendix bool, page playwright.Page) (_ []byte, written bool, _ error) {
func compile(ctx context.Context, ms *xmain.State, plugins []d2plugin.Plugin, fs fs.FS, layout *string, renderOpts d2svg.RenderOpts, fontFamily *d2fonts.FontFamily, animateInterval int64, inputPath, outputPath string, boardPath []string, noChildren, bundle, forceAppendix bool, page playwright.Page, ext exportExtension) (_ []byte, written bool, _ error) {
start := time.Now()
input, err := ms.ReadPath(inputPath)
if err != nil {
Expand Down Expand Up @@ -522,7 +538,6 @@ func compile(ctx context.Context, ms *xmain.State, plugins []d2plugin.Plugin, fs
return nil, false, err
}

ext := getExportExtension(outputPath)
switch ext {
case GIF:
svg, pngs, err := renderPNGsForGIF(ctx, ms, plugin, renderOpts, ruler, page, inputPath, diagram)
Expand Down Expand Up @@ -598,9 +613,9 @@ func compile(ctx context.Context, ms *xmain.State, plugins []d2plugin.Plugin, fs
var boards [][]byte
var err error
if noChildren {
boards, err = renderSingle(ctx, ms, compileDur, plugin, renderOpts, inputPath, outputPath, bundle, forceAppendix, page, ruler, diagram)
boards, err = renderSingle(ctx, ms, compileDur, plugin, renderOpts, inputPath, outputPath, bundle, forceAppendix, page, ruler, diagram, ext)
} else {
boards, err = render(ctx, ms, compileDur, plugin, renderOpts, inputPath, outputPath, bundle, forceAppendix, page, ruler, diagram)
boards, err = render(ctx, ms, compileDur, plugin, renderOpts, inputPath, outputPath, bundle, forceAppendix, page, ruler, diagram, ext)
}
if err != nil {
return nil, false, err
Expand Down Expand Up @@ -739,7 +754,7 @@ func relink(currDiagramPath string, d *d2target.Diagram, linkToOutput map[string
return nil
}

func render(ctx context.Context, ms *xmain.State, compileDur time.Duration, plugin d2plugin.Plugin, opts d2svg.RenderOpts, inputPath, outputPath string, bundle, forceAppendix bool, page playwright.Page, ruler *textmeasure.Ruler, diagram *d2target.Diagram) ([][]byte, error) {
func render(ctx context.Context, ms *xmain.State, compileDur time.Duration, plugin d2plugin.Plugin, opts d2svg.RenderOpts, inputPath, outputPath string, bundle, forceAppendix bool, page playwright.Page, ruler *textmeasure.Ruler, diagram *d2target.Diagram, ext exportExtension) ([][]byte, error) {
if diagram.Name != "" {
ext := filepath.Ext(outputPath)
outputPath = strings.TrimSuffix(outputPath, ext)
Expand Down Expand Up @@ -785,21 +800,21 @@ func render(ctx context.Context, ms *xmain.State, compileDur time.Duration, plug

var boards [][]byte
for _, dl := range diagram.Layers {
childrenBoards, err := render(ctx, ms, compileDur, plugin, opts, inputPath, layersOutputPath, bundle, forceAppendix, page, ruler, dl)
childrenBoards, err := render(ctx, ms, compileDur, plugin, opts, inputPath, layersOutputPath, bundle, forceAppendix, page, ruler, dl, ext)
if err != nil {
return nil, err
}
boards = append(boards, childrenBoards...)
}
for _, dl := range diagram.Scenarios {
childrenBoards, err := render(ctx, ms, compileDur, plugin, opts, inputPath, scenariosOutputPath, bundle, forceAppendix, page, ruler, dl)
childrenBoards, err := render(ctx, ms, compileDur, plugin, opts, inputPath, scenariosOutputPath, bundle, forceAppendix, page, ruler, dl, ext)
if err != nil {
return nil, err
}
boards = append(boards, childrenBoards...)
}
for _, dl := range diagram.Steps {
childrenBoards, err := render(ctx, ms, compileDur, plugin, opts, inputPath, stepsOutputPath, bundle, forceAppendix, page, ruler, dl)
childrenBoards, err := render(ctx, ms, compileDur, plugin, opts, inputPath, stepsOutputPath, bundle, forceAppendix, page, ruler, dl, ext)
if err != nil {
return nil, err
}
Expand All @@ -808,7 +823,7 @@ func render(ctx context.Context, ms *xmain.State, compileDur time.Duration, plug

if !diagram.IsFolderOnly {
start := time.Now()
out, err := _render(ctx, ms, plugin, opts, inputPath, boardOutputPath, bundle, forceAppendix, page, ruler, diagram)
out, err := _render(ctx, ms, plugin, opts, inputPath, boardOutputPath, bundle, forceAppendix, page, ruler, diagram, ext)
if err != nil {
return boards, err
}
Expand All @@ -822,9 +837,9 @@ func render(ctx context.Context, ms *xmain.State, compileDur time.Duration, plug
return boards, nil
}

func renderSingle(ctx context.Context, ms *xmain.State, compileDur time.Duration, plugin d2plugin.Plugin, opts d2svg.RenderOpts, inputPath, outputPath string, bundle, forceAppendix bool, page playwright.Page, ruler *textmeasure.Ruler, diagram *d2target.Diagram) ([][]byte, error) {
func renderSingle(ctx context.Context, ms *xmain.State, compileDur time.Duration, plugin d2plugin.Plugin, opts d2svg.RenderOpts, inputPath, outputPath string, bundle, forceAppendix bool, page playwright.Page, ruler *textmeasure.Ruler, diagram *d2target.Diagram, outputFormat exportExtension) ([][]byte, error) {
start := time.Now()
out, err := _render(ctx, ms, plugin, opts, inputPath, outputPath, bundle, forceAppendix, page, ruler, diagram)
out, err := _render(ctx, ms, plugin, opts, inputPath, outputPath, bundle, forceAppendix, page, ruler, diagram, outputFormat)
if err != nil {
return [][]byte{}, err
}
Expand All @@ -835,8 +850,9 @@ func renderSingle(ctx context.Context, ms *xmain.State, compileDur time.Duration
return [][]byte{out}, nil
}

func _render(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opts d2svg.RenderOpts, inputPath, outputPath string, bundle, forceAppendix bool, page playwright.Page, ruler *textmeasure.Ruler, diagram *d2target.Diagram) ([]byte, error) {
toPNG := getExportExtension(outputPath) == PNG
func _render(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opts d2svg.RenderOpts, inputPath, outputPath string, bundle, forceAppendix bool, page playwright.Page, ruler *textmeasure.Ruler, diagram *d2target.Diagram, outputFormat exportExtension) ([]byte, error) {
toPNG := outputFormat == PNG
Maricaya marked this conversation as resolved.
Show resolved Hide resolved

var scale *float64
if opts.Scale != nil {
scale = opts.Scale
Expand Down
4 changes: 2 additions & 2 deletions d2cli/watch.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package d2cli
import (
"context"
"embed"
_ "embed"
"errors"
"fmt"
"io/fs"
Expand Down Expand Up @@ -57,6 +56,7 @@ type watcherOpts struct {
forceAppendix bool
pw png.Playwright
fontFamily *d2fonts.FontFamily
outputFormat exportExtension
}

type watcher struct {
Expand Down Expand Up @@ -430,7 +430,7 @@ func (w *watcher) compileLoop(ctx context.Context) error {
if w.boardPath != "" {
boardPath = strings.Split(w.boardPath, string(os.PathSeparator))
}
svg, _, err := compile(ctx, w.ms, w.plugins, &fs, w.layout, w.renderOpts, w.fontFamily, w.animateInterval, w.inputPath, w.outputPath, boardPath, false, w.bundle, w.forceAppendix, w.pw.Page)
svg, _, err := compile(ctx, w.ms, w.plugins, &fs, w.layout, w.renderOpts, w.fontFamily, w.animateInterval, w.inputPath, w.outputPath, boardPath, false, w.bundle, w.forceAppendix, w.pw.Page, w.outputFormat)
w.boardpathMu.Unlock()
errs := ""
if err != nil {
Expand Down
Loading