d2/main.go

260 lines
6.6 KiB
Go
Raw Normal View History

package main
import (
"context"
"errors"
"fmt"
2022-11-29 22:20:17 +00:00
"os"
"os/exec"
"path/filepath"
"strings"
"time"
2022-11-17 01:41:53 +00:00
"github.com/playwright-community/playwright-go"
"github.com/spf13/pflag"
"go.uber.org/multierr"
"oss.terrastruct.com/d2/d2layouts/d2sequence"
"oss.terrastruct.com/d2/d2lib"
"oss.terrastruct.com/d2/d2plugin"
"oss.terrastruct.com/d2/d2renderers/d2svg"
"oss.terrastruct.com/d2/d2themes"
"oss.terrastruct.com/d2/d2themes/d2themescatalog"
2022-11-26 23:26:14 +00:00
"oss.terrastruct.com/d2/lib/imgbundler"
2022-11-17 01:41:53 +00:00
"oss.terrastruct.com/d2/lib/png"
"oss.terrastruct.com/d2/lib/textmeasure"
2022-11-07 19:39:27 +00:00
"oss.terrastruct.com/d2/lib/version"
"oss.terrastruct.com/d2/lib/xmain"
)
func main() {
xmain.Main(run)
}
func run(ctx context.Context, ms *xmain.State) (err error) {
// :(
ctx = xmain.DiscardSlog(ctx)
2022-11-17 01:13:38 +00:00
// These should be kept up-to-date with the d2 man page
2022-11-17 06:45:08 +00:00
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).")
2022-11-17 00:42:39 +00:00
if err != nil {
return err
2022-11-17 00:42:39 +00:00
}
2022-11-17 06:45:08 +00:00
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.")
2022-11-17 00:42:39 +00:00
if err != nil {
return err
2022-11-17 00:42:39 +00:00
}
debugFlag, err := ms.Opts.Bool("DEBUG", "debug", "d", false, "print debug logs.")
if err != nil {
return err
2022-11-17 00:42:39 +00:00
}
2022-11-15 00:35:22 +00:00
layoutFlag := ms.Opts.String("D2_LAYOUT", "layout", "l", "dagre", `the layout engine used.`)
2022-11-17 00:42:39 +00:00
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 err
2022-11-17 00:42:39 +00:00
}
versionFlag, err := ms.Opts.Bool("", "version", "v", false, "get the version")
if err != nil {
return err
2022-11-17 00:42:39 +00:00
}
2022-11-17 00:48:19 +00:00
err = ms.Opts.Flags.Parse(ms.Opts.Args)
if !errors.Is(err, pflag.ErrHelp) && err != nil {
return xmain.UsageErrorf("failed to parse flags: %v", err)
}
if errors.Is(err, pflag.ErrHelp) {
help(ms)
return nil
}
2022-11-17 00:48:19 +00:00
if len(ms.Opts.Flags.Args()) > 0 {
switch ms.Opts.Flags.Arg(0) {
case "layout":
2022-12-01 17:21:16 +00:00
return layoutCmd(ctx, ms)
case "fmt":
2022-12-01 17:21:16 +00:00
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")
}
var inputPath string
var outputPath string
2022-11-17 00:48:19 +00:00
if len(ms.Opts.Flags.Args()) == 0 {
if versionFlag != nil && *versionFlag {
2022-11-15 03:05:23 +00:00
fmt.Println(version.Version)
return nil
}
help(ms)
return nil
2022-11-17 00:48:19 +00:00
} else if len(ms.Opts.Flags.Args()) >= 3 {
return xmain.UsageErrorf("too many arguments passed")
}
2022-11-17 00:48:19 +00:00
if len(ms.Opts.Flags.Args()) >= 1 {
inputPath = ms.Opts.Flags.Arg(0)
}
2022-11-17 00:48:19 +00:00
if len(ms.Opts.Flags.Args()) >= 2 {
outputPath = ms.Opts.Flags.Arg(1)
} else {
if inputPath == "-" {
outputPath = "-"
} else {
outputPath = renameExt(inputPath, ".svg")
}
}
match := d2themescatalog.Find(*themeFlag)
if match == (d2themes.Theme{}) {
return xmain.UsageErrorf("-t[heme] could not be found. The available options are:\n%s\nYou provided: %d", d2themescatalog.CLIString(), *themeFlag)
}
2022-11-15 00:35:22 +00:00
ms.Log.Debug.Printf("using theme %s (ID: %d)", match.Name, *themeFlag)
2022-11-15 00:35:22 +00:00
plugin, path, err := d2plugin.FindPlugin(ctx, *layoutFlag)
if errors.Is(err, exec.ErrNotFound) {
2022-11-15 00:35:22 +00:00
return layoutNotFound(ctx, *layoutFlag)
} else if err != nil {
return err
}
pluginLocation := "bundled"
if path != "" {
pluginLocation = fmt.Sprintf("executable plugin at %s", humanPath(path))
}
2022-11-15 00:35:22 +00:00
ms.Log.Debug.Printf("using layout plugin %s (%s)", *layoutFlag, pluginLocation)
var pw png.Playwright
2022-11-17 01:41:53 +00:00
if filepath.Ext(outputPath) == ".png" {
pw, err = png.InitPlaywright()
2022-11-17 01:41:53 +00:00
if err != nil {
return err
}
2022-11-21 21:24:10 +00:00
defer func() {
cleanupErr := pw.Cleanup()
if err == nil {
err = cleanupErr
2022-11-17 22:24:59 +00:00
}
2022-11-17 02:44:43 +00:00
}()
2022-11-17 01:41:53 +00:00
}
if *watchFlag {
if inputPath == "-" {
return xmain.UsageErrorf("-w[atch] cannot be combined with reading input from stdin")
}
ms.Log.SetTS(true)
2022-11-17 07:49:45 +00:00
w, err := newWatcher(ctx, ms, watcherOpts{
layoutPlugin: plugin,
themeID: *themeFlag,
host: *hostFlag,
port: *portFlag,
inputPath: inputPath,
outputPath: outputPath,
bundle: *bundleFlag,
pw: pw,
2022-11-17 07:49:45 +00:00
})
if err != nil {
return err
}
return w.run()
}
ctx, cancel := context.WithTimeout(ctx, time.Minute*2)
defer cancel()
_, written, err := compile(ctx, ms, plugin, *themeFlag, inputPath, outputPath, *bundleFlag, pw.Page)
if err != nil {
if written {
2022-11-30 22:40:11 +00:00
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, 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, false, err
}
ruler, err := textmeasure.NewRuler()
if err != nil {
return nil, false, err
}
layout := plugin.Layout
// TODO: remove, this is just a feature flag to test sequence diagrams as we work on them
if os.Getenv("D2_SEQUENCE") == "1" {
layout = d2sequence.Layout
}
diagram, _, err := d2lib.Compile(ctx, string(input), &d2lib.CompileOptions{
Layout: layout,
Ruler: ruler,
ThemeID: themeID,
})
if err != nil {
return nil, false, err
}
svg, err := d2svg.Render(diagram)
if err != nil {
return nil, false, err
}
2022-11-21 19:09:41 +00:00
svg, err = plugin.PostProcess(ctx, svg)
if err != nil {
return svg, false, 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)
2022-11-27 02:14:41 +00:00
}
2022-11-21 19:09:41 +00:00
out := svg
2022-11-17 01:41:53 +00:00
if filepath.Ext(outputPath) == ".png" {
svg := svg
if !bundle {
var bundleErr2 error
svg, bundleErr2 = imgbundler.BundleRemote(ctx, ms, svg)
bundleErr = multierr.Combine(bundleErr, bundleErr2)
2022-11-26 23:28:34 +00:00
}
2022-11-21 19:09:41 +00:00
out, err = png.ConvertSVG(ms, page, svg)
2022-11-17 01:41:53 +00:00
if err != nil {
return svg, false, err
2022-11-17 01:41:53 +00:00
}
}
2022-11-21 18:46:54 +00:00
err = ms.WritePath(outputPath, out)
if err != nil {
return svg, false, err
2022-11-29 22:20:17 +00:00
}
return svg, true, bundleErr
}
// newExt must include leading .
func renameExt(fp string, newExt string) string {
ext := filepath.Ext(fp)
if ext == "" {
return fp + newExt
} else {
return strings.TrimSuffix(fp, ext) + newExt
}
}