diff --git a/HomebrewFormula/mermaid.rb b/HomebrewFormula/mermaid.rb new file mode 100644 index 0000000..d659611 --- /dev/null +++ b/HomebrewFormula/mermaid.rb @@ -0,0 +1,36 @@ +class Mermaid < Formula + @@tool_name = "mermaid" + @@tool_desc = "Mermaid diagrams CLI" + @@tool_path = "mermaid" + + desc "#{@@tool_desc}" + homepage "https://github.com/DavidGamba/dgtools/tree/master/#{@@tool_name}" + head "https://github.com/DavidGamba/dgtools.git", branch: "master" + + depends_on "go" => :build + + def install + cd "#{@@tool_path}" do + system "go", "get" + system "go", "build" + bin.install "#{@@tool_name}" + end + cd "HomebrewFormula" do + inreplace "completions.bash", "tool", "#{@@tool_name}" + inreplace "completions.zsh", "tool", "#{@@tool_name}" + ohai "Installing bash completion..." + bash_completion.install "completions.bash" => "dgtools.#{@@tool_name}.bash" + ohai %{Installing zsh completion... + To enable zsh completion add this to your ~/.zshrc + + \tsource #{zsh_completion.sub prefix, HOMEBREW_PREFIX}/dgtools.#{@@tool_name}.zsh + } + zsh_completion.install "completions.zsh" => "dgtools.#{@@tool_name}.zsh" + ohai "Installed #{@@tool_name} from #{@@tool_path} dir" + end + end + + test do + assert_match /Use '#{@@tool_name} help[^']*' for extra details/, shell_output("#{bin}/#{@@tool_name} --help") + end +end diff --git a/README.adoc b/README.adoc index 194f797..0b3678b 100644 --- a/README.adoc +++ b/README.adoc @@ -40,6 +40,8 @@ link:joinlines[] - Simple utility to join lines from a cli command output. link:kdecode[] - Decodes K8s secret's data block. +link:mermaid[] - Mermaid diagrams CLI. + link:password-cache[] - Cache credentials using the Linux keyring in Go. link:tz[] - Show time zones based on user defined groups. diff --git a/mermaid/README.adoc b/mermaid/README.adoc new file mode 100644 index 0000000..d4aba18 --- /dev/null +++ b/mermaid/README.adoc @@ -0,0 +1,75 @@ += mermaid: Mermaid Diagrams CLI + +Render mermaid files to SVG or PNG on the CLI + +== Install + +* Install using homebrew: ++ +---- +brew tap DavidGamba/dgtools https://github.com/DavidGamba/dgtools +brew install --HEAD DavidGamba/dgtools/mermaid +---- ++ +[NOTE] +==== +Completion is auto setup for bash. + +For `zsh` completions, an additional step is required, add the following to your `.zshrc`: + +[source, zsh] +---- +export ZSHELL="true" +source "$(brew --prefix)/share/zsh/site-functions/dgtools.mermaid.zsh" +---- +==== ++ +Upgrade with: ++ +---- +brew update +brew reinstall mermaid +---- + +* Install using go: ++ +Install the binary into your `~/go/bin`: ++ +---- +go install github.com/DavidGamba/dgtools/mermaid@latest +---- ++ +Then setup the completion. ++ +For bash: ++ +---- +complete -o default -C mermaid mermaid +---- ++ +For zsh: ++ +[source, zsh] +---- +export ZSHELL="true" +autoload -U +X compinit && compinit +autoload -U +X bashcompinit && bashcompinit +complete -o default -C mermaid mermaid +---- + +== Usage + + +. Read Mermaid file ++ +[source,sh] +---- +mermaid render -o +---- + +. Pipe Mermaid file to mermaid ++ +[source,sh] +---- +cat | mermaid render -o +---- diff --git a/mermaid/go.mod b/mermaid/go.mod new file mode 100644 index 0000000..83bbe95 --- /dev/null +++ b/mermaid/go.mod @@ -0,0 +1,20 @@ +module github.com/DavidGamba/dgtools/mermaid + +go 1.21.5 + +require ( + github.com/DavidGamba/go-getoptions v0.29.0 + github.com/dreampuf/mermaid.go v0.0.15 +) + +require ( + github.com/chromedp/cdproto v0.0.0-20240127002248-bd7a66284627 // indirect + github.com/chromedp/chromedp v0.9.3 // indirect + github.com/chromedp/sysutil v1.0.0 // indirect + github.com/gobwas/httphead v0.1.0 // indirect + github.com/gobwas/pool v0.2.1 // indirect + github.com/gobwas/ws v1.3.2 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + golang.org/x/sys v0.16.0 // indirect +) diff --git a/mermaid/go.sum b/mermaid/go.sum new file mode 100644 index 0000000..18efa4f --- /dev/null +++ b/mermaid/go.sum @@ -0,0 +1,33 @@ +github.com/DavidGamba/go-getoptions v0.29.0 h1:cU8MjOyfAyPZke4hrgEuiGBJHS9PFYPAHve2fhDhdDk= +github.com/DavidGamba/go-getoptions v0.29.0/go.mod h1:zE97E3PR9P3BI/HKyNYgdMlYxodcuiC6W68KIgeYT84= +github.com/chromedp/cdproto v0.0.0-20231011050154-1d073bb38998/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= +github.com/chromedp/cdproto v0.0.0-20240102194822-c006b26f21c7 h1:XDMhsjCzDu+sTkUz2VJxBINfDhbcoHHzJWWVqBt9WpA= +github.com/chromedp/cdproto v0.0.0-20240102194822-c006b26f21c7/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= +github.com/chromedp/cdproto v0.0.0-20240127002248-bd7a66284627 h1:L5rJ/yzLfSU3kcjsjq11xYDqAdianisL21CXQ/08Zag= +github.com/chromedp/cdproto v0.0.0-20240127002248-bd7a66284627/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= +github.com/chromedp/chromedp v0.9.3 h1:Wq58e0dZOdHsxaj9Owmfcf+ibtpYN1N0FWVbaxa/esg= +github.com/chromedp/chromedp v0.9.3/go.mod h1:NipeUkUcuzIdFbBP8eNNvl9upcceOfWzoJn6cRe4ksA= +github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic= +github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww= +github.com/dreampuf/mermaid.go v0.0.15 h1:26bhvcp3Ls2BvZMjPoZZuX49K4mClOsTAS2FtiXK8pI= +github.com/dreampuf/mermaid.go v0.0.15/go.mod h1:YsPHI3kreQyfRk0KER27VutR/EHB87nPHHrA3uVAtMI= +github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= +github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= +github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= +github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.3.0 h1:sbeU3Y4Qzlb+MOzIe6mQGf7QR4Hkv6ZD0qhGkBFL2O0= +github.com/gobwas/ws v1.3.0/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= +github.com/gobwas/ws v1.3.2 h1:zlnbNHxumkRvfPWgfXu8RBwyNR1x8wh9cf5PTOCqs9Q= +github.com/gobwas/ws v1.3.2/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo= +github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw= +github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= +golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/mermaid/main.go b/mermaid/main.go new file mode 100644 index 0000000..12506df --- /dev/null +++ b/mermaid/main.go @@ -0,0 +1,126 @@ +package main + +import ( + "context" + "errors" + "fmt" + "io" + "log" + "os" + "strings" + + "github.com/DavidGamba/go-getoptions" + mermaid_go "github.com/dreampuf/mermaid.go" +) + +var Logger = log.New(os.Stderr, "", log.LstdFlags) + +func main() { + os.Exit(program(os.Args)) +} + +func program(args []string) int { + opt := getoptions.New() + opt.Self("", `Render mermaid diagram files to SVG or PNG on the CLI + +# Read Mermaid file +mermaid render -o + +# Pipe Mermaid file to mermaid +cat | mermaid render -o +`) + + opt.Bool("quiet", false, opt.GetEnv("QUIET")) + opt.SetUnknownMode(getoptions.Pass) + + render := opt.NewCommand("render", "Render a mermaid diagram to svg or png").SetCommandFn(Render) + render.String("output", "", opt.Description("Output file.\nUse .svg or .png extension to determine output format."), opt.Required(), opt.ArgName("filename.[svg|png]")) + render.HelpSynopsisArg("[]", "mermaid input file") + + opt.HelpCommand("help", opt.Alias("?")) + remaining, err := opt.Parse(args[1:]) + if err != nil { + fmt.Fprintf(os.Stderr, "ERROR: %s\n", err) + return 1 + } + if opt.Called("quiet") { + Logger.SetOutput(io.Discard) + } + + ctx, cancel, done := getoptions.InterruptContext() + defer func() { cancel(); <-done }() + + err = opt.Dispatch(ctx, remaining) + if err != nil { + if errors.Is(err, getoptions.ErrorHelpCalled) { + return 1 + } + fmt.Fprintf(os.Stderr, "ERROR: %s\n", err) + return 1 + } + return 0 +} + +func Render(ctx context.Context, opt *getoptions.GetOpt, args []string) error { + outputFile := opt.Value("output").(string) + + re, err := mermaid_go.NewRenderEngine(ctx) + if err != nil { + return fmt.Errorf("failed to initialize render engine: %w", err) + } + defer re.Cancel() + + var reader io.Reader + if len(args) < 1 { + // Check if stdin is pipe p or device D + statStdin, _ := os.Stdin.Stat() + stdinIsDevice := (statStdin.Mode() & os.ModeDevice) != 0 + + if stdinIsDevice { + fmt.Fprint(os.Stderr, opt.Help()) + return getoptions.ErrorHelpCalled + } + Logger.Printf("Reading from stdin\n") + reader = os.Stdin + } else { + filename := args[0] + Logger.Printf("Reading from file %s\n", filename) + fh, err := os.Open(filename) + if err != nil { + return err + } + defer fh.Close() + reader = fh + } + + bytes, err := io.ReadAll(reader) + if err != nil { + return fmt.Errorf("failed to read contents: %w", err) + } + + var rendered []byte + switch { + case strings.HasSuffix(outputFile, ".svg"): + // TODO: Refactor Render to return []byte + renderedString, err := re.Render(string(bytes)) + if err != nil { + return fmt.Errorf("failed to render svg: %w", err) + } + rendered = []byte(renderedString) + case strings.HasSuffix(outputFile, ".png"): + // TODO: Refactor RenderAsPng to return []byte + rendered, _, err = re.RenderAsPng(string(bytes)) + if err != nil { + return fmt.Errorf("failed to render png: %w", err) + } + default: + return fmt.Errorf("unknown output file extension, use svg or png: %s", outputFile) + } + + err = os.WriteFile(outputFile, []byte(rendered), 0644) + if err != nil { + return fmt.Errorf("failed to write file: %w", err) + } + + return nil +}