255 lines
6.5 KiB
Go
255 lines
6.5 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"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"
|
|
"oss.terrastruct.com/d2/lib/imgbundler"
|
|
"oss.terrastruct.com/d2/lib/png"
|
|
"oss.terrastruct.com/d2/lib/textmeasure"
|
|
"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)
|
|
|
|
// 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 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 err
|
|
}
|
|
debugFlag, err := ms.Opts.Bool("DEBUG", "debug", "d", false, "print debug logs.")
|
|
if err != nil {
|
|
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 err
|
|
}
|
|
versionFlag, err := ms.Opts.Bool("", "version", "v", false, "get the version")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
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 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 *debugFlag {
|
|
ms.Env.Setenv("DEBUG", "1")
|
|
}
|
|
|
|
var inputPath string
|
|
var outputPath string
|
|
|
|
if len(ms.Opts.Flags.Args()) == 0 {
|
|
if versionFlag != nil && *versionFlag {
|
|
fmt.Println(version.Version)
|
|
return nil
|
|
}
|
|
help(ms)
|
|
return nil
|
|
} else if len(ms.Opts.Flags.Args()) >= 3 {
|
|
return xmain.UsageErrorf("too many arguments passed")
|
|
}
|
|
|
|
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 {
|
|
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)
|
|
}
|
|
ms.Log.Debug.Printf("using theme %s (ID: %d)", match.Name, *themeFlag)
|
|
|
|
plugin, path, err := d2plugin.FindPlugin(ctx, *layoutFlag)
|
|
if errors.Is(err, exec.ErrNotFound) {
|
|
return layoutNotFound(ctx, *layoutFlag)
|
|
} else if err != nil {
|
|
return err
|
|
}
|
|
|
|
pluginLocation := "bundled"
|
|
if path != "" {
|
|
pluginLocation = fmt.Sprintf("executable plugin at %s", humanPath(path))
|
|
}
|
|
ms.Log.Debug.Printf("using layout plugin %s (%s)", *layoutFlag, pluginLocation)
|
|
|
|
var pw png.Playwright
|
|
if filepath.Ext(outputPath) == ".png" {
|
|
pw, err = png.InitPlaywright()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() {
|
|
cleanupErr := pw.Cleanup()
|
|
if err == nil {
|
|
err = cleanupErr
|
|
}
|
|
}()
|
|
}
|
|
|
|
if *watchFlag {
|
|
if inputPath == "-" {
|
|
return xmain.UsageErrorf("-w[atch] cannot be combined with reading input from stdin")
|
|
}
|
|
ms.Log.SetTS(true)
|
|
w, err := newWatcher(ctx, ms, watcherOpts{
|
|
layoutPlugin: plugin,
|
|
themeID: *themeFlag,
|
|
host: *hostFlag,
|
|
port: *portFlag,
|
|
inputPath: inputPath,
|
|
outputPath: outputPath,
|
|
bundle: *bundleFlag,
|
|
pw: pw,
|
|
})
|
|
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 {
|
|
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
|
|
}
|
|
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)
|
|
}
|
|
|
|
out := svg
|
|
if filepath.Ext(outputPath) == ".png" {
|
|
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 svg, false, err
|
|
}
|
|
}
|
|
|
|
err = ms.WritePath(outputPath, out)
|
|
if err != nil {
|
|
return svg, false, err
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|