2022-11-03 13:54:49 +00:00
package main
import (
"context"
"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
2023-01-03 22:48:23 +00:00
ps , err := d2plugin . ListPlugins ( ctx )
if err != nil {
return err
}
err = populateLayoutOpts ( ctx , ms , ps )
2022-12-30 05:09:53 +00:00
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" :
2023-01-03 22:48:23 +00:00
return layoutCmd ( ctx , ms , ps )
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
2023-01-03 22:48:23 +00:00
plugin , err := d2plugin . FindPlugin ( ctx , ps , * layoutFlag )
if err != nil {
if errors . Is ( err , exec . ErrNotFound ) {
return layoutNotFound ( ctx , ps , * layoutFlag )
}
2022-11-03 13:54:49 +00:00
return err
}
2022-12-31 01:30:25 +00:00
err = d2plugin . HydratePluginOpts ( ctx , ms , plugin )
2022-12-30 05:09:53 +00:00
if err != nil {
return err
}
2023-01-03 22:48:23 +00:00
pinfo , err := plugin . Info ( ctx )
if err != nil {
return err
}
plocation := pinfo . Type
if pinfo . Type == "binary" {
plocation = fmt . Sprintf ( "executable plugin at %s" , humanPath ( pinfo . Path ) )
2022-11-03 13:54:49 +00:00
}
2023-01-03 22:48:23 +00:00
ms . Log . Debug . Printf ( "using layout plugin %s (%s)" , * layoutFlag , plocation )
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
2023-01-03 22:48:23 +00:00
func populateLayoutOpts ( ctx context . Context , ms * xmain . State , ps [ ] d2plugin . Plugin ) error {
pluginFlags , err := d2plugin . ListPluginFlags ( ctx , ps )
2022-12-30 19:33:32 +00:00
if err != nil {
return err
}
2022-12-30 08:09:28 +00:00
for _ , f := range pluginFlags {
2022-12-30 21:19:48 +00:00
f . AddToOpts ( ms . Opts )
// Don't pollute the main d2 flagset with these. It'll be a lot
ms . Opts . Flags . MarkHidden ( f . Name )
2022-12-30 06:43:01 +00:00
}
2022-12-30 08:09:28 +00:00
return nil
}