Have a single Render function handle all Markdown rendering

This commit is contained in:
Mislav Marohnić 2022-01-13 19:39:43 +01:00
parent 8198cce59b
commit 456d55ead9
11 changed files with 56 additions and 189 deletions

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -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

View file

@ -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
}

View file

@ -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")
}

View file

@ -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)
}