2022-11-03 13:54:49 +00:00
package main
import (
"context"
"errors"
"fmt"
2022-11-29 22:20:17 +00:00
"os"
2022-11-03 13:54:49 +00:00
"os/exec"
"path/filepath"
"strings"
"time"
2022-11-17 01:41:53 +00:00
"github.com/playwright-community/playwright-go"
2022-11-03 13:54:49 +00:00
"github.com/spf13/pflag"
2022-11-30 10:36:29 +00:00
"go.uber.org/multierr"
2022-11-03 13:54:49 +00:00
2022-11-26 06:32:04 +00:00
"oss.terrastruct.com/d2/d2layouts/d2sequence"
2022-12-01 05:50:55 +00:00
"oss.terrastruct.com/d2/d2lib"
2022-11-03 13:54:49 +00:00
"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"
2022-12-01 13:46:45 +00:00
"oss.terrastruct.com/d2/lib/textmeasure"
2022-11-07 19:39:27 +00:00
"oss.terrastruct.com/d2/lib/version"
2022-11-03 13:54:49 +00:00
"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 {
2022-11-30 10:35:42 +00:00
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" )
2022-11-21 19:00:26 +00:00
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 {
2022-11-30 10:35:42 +00:00
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 {
2022-11-30 10:35:42 +00:00
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 {
2022-11-30 10:35:42 +00:00
return err
2022-11-17 00:42:39 +00:00
}
versionFlag , err := ms . Opts . Bool ( "" , "version" , "v" , false , "get the version" )
if err != nil {
2022-11-30 10:35:42 +00:00
return err
2022-11-17 00:42:39 +00:00
}
2022-11-03 13:54:49 +00:00
2022-11-17 00:48:19 +00:00
err = ms . Opts . Flags . Parse ( ms . Opts . Args )
2022-11-03 13:54:49 +00:00
if ! errors . Is ( err , pflag . ErrHelp ) && err != nil {
2022-11-07 23:20:39 +00:00
return xmain . UsageErrorf ( "failed to parse flags: %v" , err )
2022-11-03 13:54:49 +00:00
}
2022-12-01 10:48:30 +00:00
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 ) {
2022-11-03 13:54:49 +00:00
case "layout" :
2022-12-01 17:21:16 +00:00
return layoutCmd ( ctx , ms )
2022-12-01 10:48:30 +00:00
case "fmt" :
2022-12-01 17:21:16 +00:00
return fmtCmd ( ctx , ms )
2022-12-01 10:48:30 +00:00
case "version" :
2022-12-01 10:57:57 +00:00
if len ( ms . Opts . Flags . Args ( ) ) > 1 {
return xmain . UsageErrorf ( "version subcommand accepts no arguments" )
}
2022-12-01 10:48:30 +00:00
fmt . Println ( version . Version )
return nil
2022-11-03 13:54:49 +00:00
}
}
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 {
2022-11-03 13:54:49 +00:00
if versionFlag != nil && * versionFlag {
2022-11-15 03:05:23 +00:00
fmt . Println ( version . Version )
2022-11-03 13:54:49 +00:00
return nil
}
help ( ms )
return nil
2022-11-17 00:48:19 +00:00
} else if len ( ms . Opts . Flags . Args ( ) ) >= 3 {
2022-11-03 13:54:49 +00:00
return xmain . UsageErrorf ( "too many arguments passed" )
}
2022-11-17 00:31:16 +00:00
2022-11-17 00:48:19 +00:00
if len ( ms . Opts . Flags . Args ( ) ) >= 1 {
inputPath = ms . Opts . Flags . Arg ( 0 )
2022-11-03 13:54:49 +00:00
}
2022-11-17 00:48:19 +00:00
if len ( ms . Opts . Flags . Args ( ) ) >= 2 {
outputPath = ms . Opts . Flags . Arg ( 1 )
2022-11-03 13:54:49 +00:00
} 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-03 13:54:49 +00:00
2022-11-15 00:35:22 +00:00
plugin , path , err := d2plugin . FindPlugin ( ctx , * layoutFlag )
2022-11-03 13:54:49 +00:00
if errors . Is ( err , exec . ErrNotFound ) {
2022-11-15 00:35:22 +00:00
return layoutNotFound ( ctx , * layoutFlag )
2022-11-03 13:54:49 +00:00
} 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 )
2022-11-03 13:54:49 +00:00
2022-11-17 19:25:07 +00:00
var pw png . Playwright
2022-11-17 01:41:53 +00:00
if filepath . Ext ( outputPath ) == ".png" {
2022-11-17 19:25:07 +00:00
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
}
2022-11-03 13:54:49 +00:00
if * watchFlag {
if inputPath == "-" {
return xmain . UsageErrorf ( "-w[atch] cannot be combined with reading input from stdin" )
}
2022-12-01 09:45:14 +00:00
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 ,
2022-11-30 10:36:29 +00:00
bundle : * bundleFlag ,
2022-11-21 19:00:26 +00:00
pw : pw ,
2022-11-17 07:49:45 +00:00
} )
2022-11-03 13:54:49 +00:00
if err != nil {
return err
}
return w . run ( )
}
ctx , cancel := context . WithTimeout ( ctx , time . Minute * 2 )
defer cancel ( )
2022-11-30 11:17:23 +00:00
_ , written , err := compile ( ctx , ms , plugin , * themeFlag , inputPath , outputPath , * bundleFlag , pw . Page )
2022-11-03 13:54:49 +00:00
if err != nil {
2022-11-30 11:17:23 +00:00
if written {
2022-11-30 22:40:11 +00:00
return fmt . Errorf ( "failed to fully compile (partial render written): %w" , err )
2022-11-30 11:17:23 +00:00
}
2022-11-30 10:36:29 +00:00
return fmt . Errorf ( "failed to compile: %w" , err )
2022-11-03 13:54:49 +00:00
}
ms . Log . Success . Printf ( "successfully compiled %v to %v" , inputPath , outputPath )
return nil
}
2022-11-30 11:17:23 +00:00
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 ) {
2022-11-03 13:54:49 +00:00
input , err := ms . ReadPath ( inputPath )
if err != nil {
2022-11-30 11:17:23 +00:00
return nil , false , err
2022-11-03 13:54:49 +00:00
}
ruler , err := textmeasure . NewRuler ( )
if err != nil {
2022-11-30 11:17:23 +00:00
return nil , false , err
2022-11-03 13:54:49 +00:00
}
2022-11-26 06:32:04 +00:00
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
}
2022-12-01 05:50:55 +00:00
diagram , _ , err := d2lib . Compile ( ctx , string ( input ) , & d2lib . CompileOptions {
2022-11-26 06:32:04 +00:00
Layout : layout ,
2022-11-03 13:54:49 +00:00
Ruler : ruler ,
ThemeID : themeID ,
} )
if err != nil {
2022-11-30 11:17:23 +00:00
return nil , false , err
2022-11-03 13:54:49 +00:00
}
2022-12-01 05:50:55 +00:00
svg , err := d2svg . Render ( diagram )
2022-11-03 13:54:49 +00:00
if err != nil {
2022-11-30 11:17:23 +00:00
return nil , false , err
2022-11-03 13:54:49 +00:00
}
2022-11-21 19:09:41 +00:00
svg , err = plugin . PostProcess ( ctx , svg )
2022-11-03 13:54:49 +00:00
if err != nil {
2022-11-30 11:17:23 +00:00
return svg , false , err
2022-11-03 13:54:49 +00:00
}
2022-11-30 10:36:29 +00:00
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-03 13:54:49 +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" {
2022-11-30 10:36:29 +00:00
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 {
2022-11-30 11:17:23 +00:00
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 )
2022-11-03 13:54:49 +00:00
if err != nil {
2022-11-30 11:17:23 +00:00
return svg , false , err
2022-11-29 22:20:17 +00:00
}
2022-11-30 11:17:23 +00:00
return svg , true , bundleErr
2022-11-03 13:54:49 +00:00
}
// 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
}
}