From 456d55ead9e0410989701e2f42bd1407ec1f1cce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Thu, 13 Jan 2022 19:39:43 +0100 Subject: [PATCH] Have a single Render function handle all Markdown rendering --- pkg/cmd/gist/view/view.go | 5 +- pkg/cmd/issue/view/view.go | 3 +- pkg/cmd/pr/review/review.go | 3 +- pkg/cmd/pr/shared/comments.go | 3 +- pkg/cmd/pr/view/view.go | 3 +- pkg/cmd/release/view/view.go | 3 +- pkg/cmd/repo/view/view.go | 3 +- pkg/cmd/root/help_reference.go | 5 +- pkg/cmd/workflow/view/view.go | 9 +-- pkg/markdown/markdown.go | 87 +++++++----------------- pkg/markdown/markdown_test.go | 121 +++++---------------------------- 11 files changed, 56 insertions(+), 189 deletions(-) diff --git a/pkg/cmd/gist/view/view.go b/pkg/cmd/gist/view/view.go index 40134412d..121ca2fac 100644 --- a/pkg/cmd/gist/view/view.go +++ b/pkg/cmd/gist/view/view.go @@ -128,8 +128,7 @@ func viewRun(opts *ViewOptions) error { return err } - theme := opts.IO.DetectTerminalTheme() - markdownStyle := markdown.GetStyle(theme) + opts.IO.DetectTerminalTheme() if err := opts.IO.StartPager(); err != nil { fmt.Fprintf(opts.IO.ErrOut, "starting pager failed: %v\n", err) } @@ -145,7 +144,7 @@ func viewRun(opts *ViewOptions) error { } if strings.Contains(gf.Type, "markdown") && !opts.Raw { - rendered, err := markdown.Render(gf.Content, markdownStyle) + rendered, err := markdown.Render(gf.Content, markdown.WithIO(opts.IO)) if err != nil { return err } diff --git a/pkg/cmd/issue/view/view.go b/pkg/cmd/issue/view/view.go index a839f488e..1eab5342c 100644 --- a/pkg/cmd/issue/view/view.go +++ b/pkg/cmd/issue/view/view.go @@ -231,8 +231,7 @@ func printHumanIssuePreview(opts *ViewOptions, issue *api.Issue) error { if issue.Body == "" { md = fmt.Sprintf("\n %s\n\n", cs.Gray("No description provided")) } else { - style := markdown.GetStyle(opts.IO.TerminalTheme()) - md, err = markdown.Render(issue.Body, style) + md, err = markdown.Render(issue.Body, markdown.WithIO(opts.IO)) if err != nil { return err } diff --git a/pkg/cmd/pr/review/review.go b/pkg/cmd/pr/review/review.go index d3b15a4ec..07593d747 100644 --- a/pkg/cmd/pr/review/review.go +++ b/pkg/cmd/pr/review/review.go @@ -273,8 +273,7 @@ func reviewSurvey(io *iostreams.IOStreams, editorCommand string) (*api.PullReque } if len(bodyAnswers.Body) > 0 { - style := markdown.GetStyle(io.DetectTerminalTheme()) - renderedBody, err := markdown.Render(bodyAnswers.Body, style) + renderedBody, err := markdown.Render(bodyAnswers.Body, markdown.WithIO(io)) if err != nil { return nil, err } diff --git a/pkg/cmd/pr/shared/comments.go b/pkg/cmd/pr/shared/comments.go index 86b52b68c..0d1d6c476 100644 --- a/pkg/cmd/pr/shared/comments.go +++ b/pkg/cmd/pr/shared/comments.go @@ -123,8 +123,7 @@ func formatComment(io *iostreams.IOStreams, comment Comment, newest bool) (strin if comment.Content() == "" { md = fmt.Sprintf("\n %s\n\n", cs.Gray("No body provided")) } else { - style := markdown.GetStyle(io.TerminalTheme()) - md, err = markdown.Render(comment.Content(), style) + md, err = markdown.Render(comment.Content(), markdown.WithIO(io)) if err != nil { return "", err } diff --git a/pkg/cmd/pr/view/view.go b/pkg/cmd/pr/view/view.go index bc3f517e4..f0746772d 100644 --- a/pkg/cmd/pr/view/view.go +++ b/pkg/cmd/pr/view/view.go @@ -215,8 +215,7 @@ func printHumanPrPreview(opts *ViewOptions, pr *api.PullRequest) error { if pr.Body == "" { md = fmt.Sprintf("\n %s\n\n", cs.Gray("No description provided")) } else { - style := markdown.GetStyle(opts.IO.TerminalTheme()) - md, err = markdown.Render(pr.Body, style) + md, err = markdown.Render(pr.Body, markdown.WithIO(opts.IO)) if err != nil { return err } diff --git a/pkg/cmd/release/view/view.go b/pkg/cmd/release/view/view.go index ae7d10e97..dde802f8d 100644 --- a/pkg/cmd/release/view/view.go +++ b/pkg/cmd/release/view/view.go @@ -141,8 +141,7 @@ func renderReleaseTTY(io *iostreams.IOStreams, release *shared.Release) error { fmt.Fprintf(w, "%s\n", iofmt.Gray(fmt.Sprintf("%s released this %s", release.Author.Login, utils.FuzzyAgo(time.Since(*release.PublishedAt))))) } - style := markdown.GetStyle(io.TerminalTheme()) - renderedDescription, err := markdown.Render(release.Body, style) + renderedDescription, err := markdown.Render(release.Body, markdown.WithIO(io)) if err != nil { return err } diff --git a/pkg/cmd/repo/view/view.go b/pkg/cmd/repo/view/view.go index 3d2879dc0..2a1c0fe30 100644 --- a/pkg/cmd/repo/view/view.go +++ b/pkg/cmd/repo/view/view.go @@ -187,8 +187,7 @@ func viewRun(opts *ViewOptions) error { readmeContent = cs.Gray("This repository does not have a README") } else if isMarkdownFile(readme.Filename) { var err error - style := markdown.GetStyle(opts.IO.TerminalTheme()) - readmeContent, err = markdown.RenderWithBaseURL(readme.Content, style, readme.BaseURL) + readmeContent, err = markdown.Render(readme.Content, markdown.WithIO(opts.IO), markdown.WithBaseURL(readme.BaseURL)) if err != nil { return fmt.Errorf("error rendering markdown: %w", err) } diff --git a/pkg/cmd/root/help_reference.go b/pkg/cmd/root/help_reference.go index 73070e0e9..b12f33b99 100644 --- a/pkg/cmd/root/help_reference.go +++ b/pkg/cmd/root/help_reference.go @@ -14,13 +14,12 @@ import ( func referenceHelpFn(io *iostreams.IOStreams) func(*cobra.Command, []string) { return func(cmd *cobra.Command, args []string) { wrapWidth := 0 - style := "notty" if io.IsStdoutTTY() { + io.DetectTerminalTheme() wrapWidth = io.TerminalWidth() - style = markdown.GetStyle(io.DetectTerminalTheme()) } - md, err := markdown.RenderWithWrap(cmd.Long, style, wrapWidth) + md, err := markdown.Render(cmd.Long, markdown.WithIO(io), markdown.WithWrap(wrapWidth)) if err != nil { fmt.Fprintln(io.ErrOut, err) return diff --git a/pkg/cmd/workflow/view/view.go b/pkg/cmd/workflow/view/view.go index d6b71e6c1..210835759 100644 --- a/pkg/cmd/workflow/view/view.go +++ b/pkg/cmd/workflow/view/view.go @@ -156,8 +156,7 @@ func viewWorkflowContent(opts *ViewOptions, client *api.Client, workflow *shared yaml := string(yamlBytes) - theme := opts.IO.DetectTerminalTheme() - markdownStyle := markdown.GetStyle(theme) + opts.IO.DetectTerminalTheme() if err := opts.IO.StartPager(); err != nil { fmt.Fprintf(opts.IO.ErrOut, "starting pager failed: %v\n", err) } @@ -172,11 +171,7 @@ func viewWorkflowContent(opts *ViewOptions, client *api.Client, workflow *shared fmt.Fprintf(out, "ID: %s", cs.Cyanf("%d", workflow.ID)) codeBlock := fmt.Sprintf("```yaml\n%s\n```", yaml) - rendered, err := markdown.RenderWithOpts(codeBlock, markdownStyle, - markdown.RenderOpts{ - markdown.WithoutIndentation(), - markdown.WithoutWrap(), - }) + rendered, err := markdown.Render(codeBlock, markdown.WithIO(opts.IO), markdown.WithoutIndentation(), markdown.WithWrap(0)) if err != nil { return err } diff --git a/pkg/markdown/markdown.go b/pkg/markdown/markdown.go index 9da1a2586..cd578bf06 100644 --- a/pkg/markdown/markdown.go +++ b/pkg/markdown/markdown.go @@ -7,8 +7,6 @@ import ( "github.com/charmbracelet/glamour" ) -type RenderOpts []glamour.TermRendererOption - func WithoutIndentation() glamour.TermRendererOption { overrides := []byte(` { @@ -23,15 +21,38 @@ func WithoutIndentation() glamour.TermRendererOption { return glamour.WithStylesFromJSONBytes(overrides) } -func WithoutWrap() glamour.TermRendererOption { - return glamour.WithWordWrap(0) +func WithWrap(w int) glamour.TermRendererOption { + return glamour.WithWordWrap(w) } -func render(text string, opts RenderOpts) (string, error) { +type IOStreams interface { + TerminalTheme() string +} + +func WithIO(io IOStreams) glamour.TermRendererOption { + style := os.Getenv("GLAMOUR_STYLE") + if style == "" || style == "auto" { + theme := io.TerminalTheme() + switch theme { + case "light", "dark": + style = theme + default: + style = "notty" + } + } + return glamour.WithStylePath(style) +} + +func WithBaseURL(u string) glamour.TermRendererOption { + return glamour.WithBaseURL(u) +} + +func Render(text string, opts ...glamour.TermRendererOption) (string, error) { // Glamour rendering preserves carriage return characters in code blocks, but // we need to ensure that no such characters are present in the output. text = strings.ReplaceAll(text, "\r\n", "\n") + opts = append(opts, glamour.WithEmoji()) tr, err := glamour.NewTermRenderer(opts...) if err != nil { return "", err @@ -39,59 +60,3 @@ func render(text string, opts RenderOpts) (string, error) { return tr.Render(text) } - -func Render(text, style string) (string, error) { - opts := RenderOpts{ - glamour.WithStylePath(style), - glamour.WithEmoji(), - } - - return render(text, opts) -} - -func RenderWithOpts(text, style string, opts RenderOpts) (string, error) { - defaultOpts := RenderOpts{ - glamour.WithStylePath(style), - glamour.WithEmoji(), - } - opts = append(defaultOpts, opts...) - - return render(text, opts) -} - -func RenderWithBaseURL(text, style, baseURL string) (string, error) { - opts := RenderOpts{ - glamour.WithStylePath(style), - glamour.WithEmoji(), - glamour.WithBaseURL(baseURL), - } - - return render(text, opts) -} - -func RenderWithWrap(text, style string, wrap int) (string, error) { - opts := RenderOpts{ - glamour.WithStylePath(style), - glamour.WithEmoji(), - glamour.WithWordWrap(wrap), - } - - return render(text, opts) -} - -func GetStyle(defaultStyle string) string { - style := fromEnv() - if style != "" && style != "auto" { - return style - } - - if defaultStyle == "light" || defaultStyle == "dark" { - return defaultStyle - } - - return "notty" -} - -var fromEnv = func() string { - return os.Getenv("GLAMOUR_STYLE") -} diff --git a/pkg/markdown/markdown_test.go b/pkg/markdown/markdown_test.go index 079b5ff7a..77b56d6e9 100644 --- a/pkg/markdown/markdown_test.go +++ b/pkg/markdown/markdown_test.go @@ -1,50 +1,46 @@ package markdown import ( + "os" "testing" "github.com/stretchr/testify/assert" ) func Test_Render(t *testing.T) { + os.Unsetenv("GLAMOUR_STYLE") + type input struct { text string - style string - } - type output struct { - wantsErr bool + theme string } tests := []struct { - name string - input input - output output + name string + input input + wantsErr bool }{ { - name: "invalid glamour style", + name: "light theme", input: input{ text: "some text", - style: "invalid", - }, - output: output{ - wantsErr: true, + theme: "light", }, + wantsErr: false, }, { - name: "valid glamour style", + name: "dark theme", input: input{ text: "some text", - style: "dark", - }, - output: output{ - wantsErr: false, + theme: "dark", }, + wantsErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - _, err := Render(tt.input.text, tt.input.style) - if tt.output.wantsErr { + _, err := Render(tt.input.text, WithIO(terminalThemer(tt.input.theme))) + if tt.wantsErr { assert.Error(t, err) return } @@ -53,89 +49,8 @@ func Test_Render(t *testing.T) { } } -func Test_GetStyle(t *testing.T) { - type input struct { - glamourStyle string - terminalTheme string - } - type output struct { - style string - } - tests := []struct { - name string - input input - output output - }{ - { - name: "no glamour style and no terminal theme", - input: input{ - glamourStyle: "", - terminalTheme: "none", - }, - output: output{ - style: "notty", - }, - }, - { - name: "auto glamour style and no terminal theme", - input: input{ - glamourStyle: "auto", - terminalTheme: "none", - }, - output: output{ - style: "notty", - }, - }, - { - name: "user glamour style and no terminal theme", - input: input{ - glamourStyle: "somestyle", - terminalTheme: "none", - }, - output: output{ - style: "somestyle", - }, - }, - { - name: "no glamour style and light terminal theme", - input: input{ - glamourStyle: "", - terminalTheme: "light", - }, - output: output{ - style: "light", - }, - }, - { - name: "no glamour style and dark terminal theme", - input: input{ - glamourStyle: "", - terminalTheme: "dark", - }, - output: output{ - style: "dark", - }, - }, - { - name: "no glamour style and unknown terminal theme", - input: input{ - glamourStyle: "", - terminalTheme: "unknown", - }, - output: output{ - style: "notty", - }, - }, - } +type terminalThemer string - for _, tt := range tests { - fromEnv = func() string { - return tt.input.glamourStyle - } - - t.Run(tt.name, func(t *testing.T) { - style := GetStyle(tt.input.terminalTheme) - assert.Equal(t, tt.output.style, style) - }) - } +func (tt terminalThemer) TerminalTheme() string { + return string(tt) }