2022-11-03 13:54:49 +00:00
package main
import (
"context"
"errors"
"fmt"
"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"
"oss.terrastruct.com/d2"
"oss.terrastruct.com/d2/d2plugin"
"oss.terrastruct.com/d2/d2renderers/d2svg"
"oss.terrastruct.com/d2/d2renderers/textmeasure"
"oss.terrastruct.com/d2/d2themes"
"oss.terrastruct.com/d2/d2themes/d2themescatalog"
2022-11-17 01:41:53 +00:00
"oss.terrastruct.com/d2/lib/png"
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 {
return xmain . UsageErrorf ( err . Error ( ) )
}
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 {
return xmain . UsageErrorf ( err . Error ( ) )
}
debugFlag , err := ms . Opts . Bool ( "DEBUG" , "debug" , "d" , false , "print debug logs." )
if err != nil {
return xmain . UsageErrorf ( err . Error ( ) )
}
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 xmain . UsageErrorf ( err . Error ( ) )
}
versionFlag , err := ms . Opts . Bool ( "" , "version" , "v" , false , "get the version" )
if err != nil {
return xmain . UsageErrorf ( err . Error ( ) )
}
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-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" :
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
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 {
if ms . Opts . Flags . Arg ( 0 ) == "version" {
2022-11-15 03:05:23 +00:00
fmt . Println ( version . Version )
2022-11-03 13:54:49 +00:00
return nil
}
2022-11-17 00:48:19 +00:00
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" )
}
ms . Env . Setenv ( "LOG_TIMESTAMPS" , "1" )
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-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 ( )
if * bundleFlag {
_ = 343
}
2022-11-21 19:00:26 +00:00
_ , err = compile ( ctx , ms , plugin , * themeFlag , inputPath , outputPath , pw . Page )
2022-11-03 13:54:49 +00:00
if err != nil {
return err
}
ms . Log . Success . Printf ( "successfully compiled %v to %v" , inputPath , outputPath )
return nil
}
2022-11-21 19:00:26 +00:00
func compile ( ctx context . Context , ms * xmain . State , plugin d2plugin . Plugin , themeID int64 , inputPath , outputPath string , page playwright . Page ) ( [ ] byte , error ) {
2022-11-03 13:54:49 +00:00
input , err := ms . ReadPath ( inputPath )
if err != nil {
return nil , err
}
ruler , err := textmeasure . NewRuler ( )
if err != nil {
return nil , err
}
d , err := d2 . Compile ( ctx , string ( input ) , & d2 . CompileOptions {
Layout : plugin . Layout ,
Ruler : ruler ,
ThemeID : themeID ,
} )
if err != nil {
return nil , err
}
svg , err := d2svg . Render ( d )
if err != nil {
return nil , err
}
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 {
return nil , err
}
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-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 nil , err
}
}
2022-11-21 18:46:54 +00:00
err = ms . WritePath ( outputPath , out )
2022-11-03 13:54:49 +00:00
if err != nil {
return nil , err
}
return svg , nil
}
// 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
}
}