d2/main.go
Anmol Sethi c4ef432daf
Move textmeasure into lib
It's not a d2renderer.
2022-12-01 06:51:17 -08:00

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
}
}