2022-11-03 13:54:49 +00:00
package main
import (
"context"
2022-12-30 08:09:28 +00:00
"encoding/json"
2022-11-03 13:54:49 +00:00
"errors"
"fmt"
2022-12-01 19:19:39 +00:00
"io"
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-12-22 05:58:56 +00:00
"oss.terrastruct.com/util-go/go2"
2022-12-01 19:19:39 +00:00
"oss.terrastruct.com/util-go/xmain"
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"
2022-12-22 05:58:56 +00:00
"oss.terrastruct.com/d2/d2renderers/d2fonts"
2022-11-03 13:54:49 +00:00
"oss.terrastruct.com/d2/d2renderers/d2svg"
2022-12-28 04:29:51 +00:00
"oss.terrastruct.com/d2/d2renderers/d2svg/appendix"
2022-11-03 13:54:49 +00:00
"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-12-01 19:19:39 +00:00
ctxlog "oss.terrastruct.com/d2/lib/log"
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-12-01 19:19:39 +00:00
"cdr.dev/slog"
"cdr.dev/slog/sloggers/sloghuman"
2022-11-03 13:54:49 +00:00
)
func main ( ) {
xmain . Main ( run )
}
func run ( ctx context . Context , ms * xmain . State ) ( err error ) {
// :(
2022-12-01 19:19:39 +00:00
ctx = DiscardSlog ( ctx )
2022-11-03 13:54:49 +00:00
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-12-21 07:43:45 +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-12-21 07:43:45 +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
}
2022-12-12 07:31:01 +00:00
padFlag , err := ms . Opts . Int64 ( "D2_PAD" , "pad" , "" , d2svg . DEFAULT_PADDING , "pixels padded around the rendered diagram" )
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 {
2022-11-30 10:35:42 +00:00
return err
2022-11-17 00:42:39 +00:00
}
2022-12-21 07:43:45 +00:00
sketchFlag , err := ms . Opts . Bool ( "D2_SKETCH" , "sketch" , "s" , false , "render the diagram to look like it was sketched by hand" )
if err != nil {
return err
}
2022-11-03 13:54:49 +00:00
2022-12-30 05:09:53 +00:00
err = populateLayoutOpts ( ms )
if err != nil {
return err
}
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
}
2022-12-30 08:09:28 +00:00
err = parseLayoutOpts ( ms , plugin )
2022-12-30 05:09:53 +00:00
if err != nil {
return err
}
2022-11-03 13:54:49 +00:00
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 ,
2022-12-21 07:43:45 +00:00
sketch : * sketchFlag ,
2022-11-17 07:49:45 +00:00
themeID : * themeFlag ,
2022-12-12 07:31:01 +00:00
pad : * padFlag ,
2022-11-17 07:49:45 +00:00
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-12-21 07:43:45 +00:00
_ , written , err := compile ( ctx , ms , plugin , * sketchFlag , * padFlag , * 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-12-21 07:43:45 +00:00
func compile ( ctx context . Context , ms * xmain . State , plugin d2plugin . Plugin , sketch bool , pad , 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
2022-12-22 05:58:56 +00:00
opts := & d2lib . CompileOptions {
2022-11-26 06:32:04 +00:00
Layout : layout ,
2022-11-03 13:54:49 +00:00
Ruler : ruler ,
ThemeID : themeID ,
2022-12-22 05:58:56 +00:00
}
if sketch {
opts . FontFamily = go2 . Pointer ( d2fonts . HandDrawn )
}
diagram , _ , err := d2lib . Compile ( ctx , string ( input ) , opts )
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-12-21 07:43:45 +00:00
svg , err := d2svg . Render ( diagram , & d2svg . RenderOpts {
Pad : int ( pad ) ,
Sketch : sketch ,
} )
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-12-21 07:43:45 +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-12-29 00:42:22 +00:00
svg := appendix . Append ( diagram , ruler , svg )
2022-12-28 04:29:51 +00:00
2022-11-30 10:36:29 +00:00
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-12-18 18:48:57 +00:00
} else {
2022-12-18 20:26:58 +00:00
if len ( out ) > 0 && out [ len ( out ) - 1 ] != '\n' {
2022-12-18 18:48:57 +00:00
out = append ( out , '\n' )
}
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-03 13:54:49 +00:00
}
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
}
}
2022-12-01 19:19:39 +00:00
// TODO: remove after removing slog
func DiscardSlog ( ctx context . Context ) context . Context {
return ctxlog . With ( ctx , slog . Make ( sloghuman . Sink ( io . Discard ) ) )
}
2022-12-30 05:09:53 +00:00
func populateLayoutOpts ( ms * xmain . State ) error {
2022-12-30 08:09:28 +00:00
pluginFlags := d2plugin . ListPluginFlags ( )
for _ , f := range pluginFlags {
switch f . Type {
case "string" :
ms . Opts . String ( "" , f . Name , "" , f . Default . ( string ) , f . Usage )
case "int64" :
ms . Opts . Int64 ( "" , f . Name , "" , f . Default . ( int64 ) , f . Usage )
}
2022-12-30 06:43:01 +00:00
}
2022-12-30 08:09:28 +00:00
return nil
}
func parseLayoutOpts ( ms * xmain . State , plugin d2plugin . Plugin ) error {
opts := make ( map [ string ] interface { } )
for _ , f := range plugin . Flags ( ) {
switch f . Type {
case "string" :
val , _ := ms . Opts . Flags . GetString ( f . Name )
opts [ f . Tag ] = val
case "int64" :
val , _ := ms . Opts . Flags . GetInt64 ( f . Name )
opts [ f . Tag ] = val
}
2022-12-30 06:43:01 +00:00
}
2022-12-30 08:09:28 +00:00
b , err := json . Marshal ( opts )
2022-12-30 06:43:01 +00:00
if err != nil {
return err
}
2022-12-30 08:09:28 +00:00
err = plugin . HydrateOpts ( b )
return err
// switch layout {
// case "dagre":
// nodesep, _ := ms.Opts.Flags.GetInt64("dagre-nodesep")
// edgesep, _ := ms.Opts.Flags.GetInt64("dagre-edgesep")
// return d2dagrelayout.Opts{
// NodeSep: int(nodesep),
// EdgeSep: int(edgesep),
// }, nil
// case "elk":
// algorithm, _ := ms.Opts.Flags.GetString("elk-algorithm")
// nodeSpacing, _ := ms.Opts.Flags.GetInt64("elk-nodeNodeBetweenLayers")
// padding, _ := ms.Opts.Flags.GetString("elk-padding")
// edgeNodeSpacing, _ := ms.Opts.Flags.GetInt64("elk-edgeNodeSpacing")
// selfLoopSpacing, _ := ms.Opts.Flags.GetInt64("elk-nodeSelfLoop")
// return d2elklayout.ConfigurableOpts{
// Algorithm: algorithm,
// NodeSpacing: int(nodeSpacing),
// Padding: padding,
// EdgeNodeSpacing: int(edgeNodeSpacing),
// SelfLoopSpacing: int(selfLoopSpacing),
// }, nil
// default:
//
// }
//
// return nil, fmt.Errorf("unexpected error, layout not found for parsing opts")
2022-12-30 05:09:53 +00:00
}