From 4925c3cf01b61d3c625abcacff157afdfb4fa7ba Mon Sep 17 00:00:00 2001 From: vilmibm Date: Tue, 7 Jan 2020 11:48:52 -0600 Subject: [PATCH 1/8] preview PRs and issues in the terminal with -p --- api/queries_issue.go | 28 ++++++++++++++++++---- api/queries_pr.go | 25 +++++++++++++++++++- command/issue.go | 37 +++++++++++++++++++++++++++-- command/pr.go | 39 ++++++++++++++++++++++++++---- go.mod | 2 +- go.sum | 56 ++++++++++++++++++++++++++++++++++++++++++-- utils/utils.go | 38 ++++++++++++++++++++++++++++++ 7 files changed, 211 insertions(+), 14 deletions(-) diff --git a/api/queries_issue.go b/api/queries_issue.go index 43faa7321..39a998199 100644 --- a/api/queries_issue.go +++ b/api/queries_issue.go @@ -16,10 +16,17 @@ type IssuesAndTotalCount struct { } type Issue struct { - Number int - Title string - URL string - State string + Number int + Title string + URL string + State string + Body string + Comments struct { + TotalCount int + } + Author struct { + Login string + } Labels struct { Nodes []IssueLabel @@ -234,6 +241,19 @@ func IssueByNumber(client *Client, ghRepo Repo, number int) (*Issue, error) { query($owner: String!, $repo: String!, $issue_number: Int!) { repository(owner: $owner, name: $repo) { issue(number: $issue_number) { + title + body + author { + login + } + comments { + totalCount + } + labels(first: 3) { + nodes { + name + } + } number url } diff --git a/api/queries_pr.go b/api/queries_pr.go index 22d0ec3fd..9603bf0f2 100644 --- a/api/queries_pr.go +++ b/api/queries_pr.go @@ -21,8 +21,13 @@ type PullRequest struct { Title string State string URL string + BaseRefName string HeadRefName string + Body string + Author struct { + Login string + } HeadRepositoryOwner struct { Login string } @@ -38,7 +43,8 @@ type PullRequest struct { ReviewDecision string Commits struct { - Nodes []struct { + TotalCount int + Nodes []struct { Commit struct { StatusCheckRollup struct { Contexts struct { @@ -296,6 +302,15 @@ func PullRequestByNumber(client *Client, ghRepo Repo, number int) (*PullRequest, pullRequest(number: $pr_number) { url number + title + body + author { + login + } + commits { + totalCount + } + baseRefName headRefName headRepositoryOwner { login @@ -343,7 +358,15 @@ func PullRequestForBranch(client *Client, ghRepo Repo, branch string) (*PullRequ nodes { number title + body + author { + login + } + commits { + totalCount + } url + baseRefName headRefName headRepositoryOwner { login diff --git a/command/issue.go b/command/issue.go index cc114d728..02c5b5adf 100644 --- a/command/issue.go +++ b/command/issue.go @@ -34,6 +34,8 @@ func init() { issueListCmd.Flags().StringSliceP("label", "l", nil, "Filter by label") issueListCmd.Flags().StringP("state", "s", "", "Filter by state: {open|closed|all}") issueListCmd.Flags().IntP("limit", "L", 30, "Maximum number of issues to fetch") + + issueViewCmd.Flags().BoolP("preview", "p", false, "Preview PR in termianl") } var issueCmd = &cobra.Command{ @@ -217,8 +219,39 @@ func issueView(cmd *cobra.Command, args []string) error { } openURL := issue.URL - fmt.Fprintf(cmd.ErrOrStderr(), "Opening %s in your browser.\n", openURL) - return utils.OpenInBrowser(openURL) + preview, err := cmd.Flags().GetBool("preview") + if err != nil { + return err + } + + if preview { + coloredLabels := labelList(*issue) + if coloredLabels != "" { + coloredLabels = utils.Gray(fmt.Sprintf(" (%s)", coloredLabels)) + } + meta := "opened by %s. %d comment" + if issue.Comments.TotalCount != 1 { + meta += "s." + } else { + meta += "." + } + meta += coloredLabels + + fmt.Println(utils.Bold(issue.Title)) + fmt.Println(utils.Gray(fmt.Sprintf(meta, + issue.Author.Login, + issue.Comments.TotalCount, + ))) + fmt.Println() + fmt.Println(utils.RenderMarkdown(issue.Body)) + fmt.Println() + fmt.Println(utils.Gray(fmt.Sprintf("View this issue on GitHub: %s", openURL))) + return nil + } else { + fmt.Fprintf(cmd.ErrOrStderr(), "Opening %s in your browser.\n", openURL) + return utils.OpenInBrowser(openURL) + } + } var issueURLRE = regexp.MustCompile(`^https://github\.com/([^/]+)/([^/]+)/issues/(\d+)`) diff --git a/command/pr.go b/command/pr.go index cdf5dd6bc..82bbec7b2 100644 --- a/command/pr.go +++ b/command/pr.go @@ -31,6 +31,8 @@ func init() { prListCmd.Flags().StringP("base", "B", "", "Filter by base branch") prListCmd.Flags().StringSliceP("label", "l", nil, "Filter by label") prListCmd.Flags().StringP("assignee", "a", "", "Filter by assignee") + + prViewCmd.Flags().BoolP("preview", "p", false, "Preview PR in termianl") } var prCmd = &cobra.Command{ @@ -253,9 +255,15 @@ func prView(cmd *cobra.Command, args []string) error { return err } + preview, err := cmd.Flags().GetBool("preview") + if err != nil { + return err + } + var openURL string + var pr *api.PullRequest if len(args) > 0 { - pr, err := prFromArg(apiClient, baseRepo, args[0]) + pr, err = prFromArg(apiClient, baseRepo, args[0]) if err != nil { return err } @@ -269,7 +277,7 @@ func prView(cmd *cobra.Command, args []string) error { if prNumber > 0 { openURL = fmt.Sprintf("https://github.com/%s/%s/pull/%d", baseRepo.RepoOwner(), baseRepo.RepoName(), prNumber) } else { - pr, err := api.PullRequestForBranch(apiClient, baseRepo, branchWithOwner) + pr, err = api.PullRequestForBranch(apiClient, baseRepo, branchWithOwner) if err != nil { var notFoundErr *api.NotFoundError if errors.As(err, ¬FoundErr) { @@ -282,8 +290,31 @@ func prView(cmd *cobra.Command, args []string) error { } } - fmt.Fprintf(cmd.ErrOrStderr(), "Opening %s in your browser.\n", openURL) - return utils.OpenInBrowser(openURL) + if preview { + meta := "%s wants to merge %d commit" + if pr.Commits.TotalCount == 1 { + meta += " " + } else { + meta += "s " + } + meta += "into %s from %s" + fmt.Println(utils.Bold(pr.Title)) + fmt.Println(utils.Gray(fmt.Sprintf(meta, + pr.Author.Login, + pr.Commits.TotalCount, + pr.BaseRefName, + pr.HeadRefName, + ))) + fmt.Println() + fmt.Println(utils.RenderMarkdown(pr.Body)) + fmt.Println() + fmt.Println(utils.Gray(fmt.Sprintf("View this PR on GitHub: %s", openURL))) + + return nil + } else { + fmt.Fprintf(cmd.ErrOrStderr(), "Opening %s in your browser.\n", openURL) + return utils.OpenInBrowser(openURL) + } } var prURLRE = regexp.MustCompile(`^https://github\.com/([^/]+)/([^/]+)/pull/(\d+)`) diff --git a/go.mod b/go.mod index 361ad8acf..813f8538e 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/pkg/errors v0.8.1 github.com/spf13/cobra v0.0.5 github.com/spf13/pflag v1.0.5 - github.com/stretchr/testify v1.3.0 // indirect + github.com/tj/go-termd v0.0.1 golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5 gopkg.in/yaml.v3 v3.0.0-20191010095647-fc94e3f71652 ) diff --git a/go.sum b/go.sum index 9bc2c3c17..9d079db48 100644 --- a/go.sum +++ b/go.sum @@ -2,34 +2,65 @@ github.com/AlecAivazis/survey/v2 v2.0.4 h1:qzXnJSzXEvmUllWqMBWpZndvT2YfoAUzAMvZU github.com/AlecAivazis/survey/v2 v2.0.4/go.mod h1:WYBhg6f0y/fNYUuesWQc0PKbJcEliGcYHB9sNT3Bg74= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0= +github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0= github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8 h1:xzYJEypr/85nBpB11F9br+3HUrpgb+fcm5iADzXXYEw= github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc= +github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= +github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 h1:smF2tmSOzy2Mm+0dGI2AIUHY+w0BUc+4tn40djz7+6U= +github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI= +github.com/alecthomas/chroma v0.6.8 h1:TW4JJaIdbAbMyUtGEd6BukFlOKYvVQz3vVhLBEUNwMU= +github.com/alecthomas/chroma v0.6.8/go.mod h1:o9ohftueRi7H5be3+Q2cQCNa/YnLBFUNx40ZJfGVFKA= +github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721 h1:JHZL0hZKJ1VENNfmXvHbgYlbUOvpzYzvy2aZU5gXVeo= +github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0= +github.com/alecthomas/kong v0.1.17-0.20190424132513-439c674f7ae0/go.mod h1:+inYUSluD+p4L8KdviBSgzcqEjUQOfC5fQDRFuc36lI= +github.com/alecthomas/kong v0.2.1-0.20190708041108-0548c6b1afae/go.mod h1:+inYUSluD+p4L8KdviBSgzcqEjUQOfC5fQDRFuc36lI= +github.com/alecthomas/kong-hcl v0.1.8-0.20190615233001-b21fea9723c8/go.mod h1:MRgZdU3vrFd05IQ89AxUZ0aYdF39BYoNFa324SodPCA= +github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897 h1:p9Sln00KOTlrYkxI1zYWl1QLnEqAqEARBEYa8FQnQcY= +github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59 h1:WWB576BN5zNSZc/M9d/10pqEx5VHNhaQ/yOVAkmj5Yo= +github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E= +github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ= +github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dlclark/regexp2 v1.1.6 h1:CqB4MjHw0MFCDj+PHHjiESmHX+N7t0tJzKvC6M97BRg= +github.com/dlclark/regexp2 v1.1.6/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/hashicorp/go-version v1.2.0 h1:3vNe/fWF5CBgRIguda1meWhsZHy3m8gCJ5wx+dIzX/E= -github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +github.com/gorilla/csrf v1.6.0/go.mod h1:7tSf8kmjNYr7IWDCYhd3U8Ck34iQ/Yw5CJu7bAkHEGI= +github.com/gorilla/handlers v1.4.1/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= +github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/hashicorp/go-version v1.2.0 h1:3vNe/fWF5CBgRIguda1meWhsZHy3m8gCJ5wx+dIzX/E= +github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174 h1:WlZsjVhE8Af9IcZDGgJGQpNflI3+MJSBhsgT5PCtzBQ= github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.4 h1:5Myjjh3JY/NaAi4IsUbHADytDyl1VE1Y9PXDlL+P/VQ= github.com/kr/pty v1.1.4/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= @@ -37,14 +68,24 @@ github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1f github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4= +github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/russross/blackfriday v2.0.0+incompatible h1:cBXrhZNUf9C+La9/YpS+UHpUT8YD6Td9ZMSU9APFcsk= +github.com/russross/blackfriday v2.0.0+incompatible/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s= @@ -60,7 +101,17 @@ github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/tj/assert v0.0.0-20190920132354-ee03d75cd160 h1:NSWpaDaurcAJY7PkL8Xt0PhZE7qpvbZl5ljd8r6U0bI= +github.com/tj/assert v0.0.0-20190920132354-ee03d75cd160/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0= +github.com/tj/go-css v0.0.0-20191108133013-220a796d1705 h1:+UA89aFRjPMqdccHd9A0HLNCRDXIoElaDoW2C1V3TzA= +github.com/tj/go-css v0.0.0-20191108133013-220a796d1705/go.mod h1:e+JPLQ9wyQCgRnPenX2bo7MJoLphBHz5c1WUqaANSeA= +github.com/tj/go-termd v0.0.1 h1:NRrUrpzPj3jVlimGNMdnNOry0vYgvEkMJcJWZkKAeZI= +github.com/tj/go-termd v0.0.1/go.mod h1:qf28T7t3aasdTnAz6ehff7dfebsK+lAKK53duclZ/yQ= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 h1:mKdxBk7AujPs8kU4m80U72y/zjbZ3UcXC7dClwKbUI0= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -68,6 +119,7 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5 h1:8dUaAV7K4uHsF56JQWkprecIQKdPHtR9jCHF5nB8uzc= golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= diff --git a/utils/utils.go b/utils/utils.go index 6a1730cb8..5daa4710d 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -1,14 +1,36 @@ package utils import ( + "bytes" "errors" "os" "os/exec" "runtime" "github.com/kballard/go-shellquote" + md "github.com/tj/go-termd" ) +var mdCompiler md.Compiler + +func init() { + mdCompiler = md.Compiler{ + Columns: 100, + SyntaxHighlighter: md.SyntaxTheme{ + "keyword": md.Style{Color: "#9196ed"}, + "comment": md.Style{ + Color: "#c0c0c2", + }, + "literal": md.Style{ + Color: "#aaedf7", + }, + "name": md.Style{ + Color: "#fe8eb5", + }, + }, + } +} + func OpenInBrowser(url string) error { browser := os.Getenv("BROWSER") if browser == "" { @@ -51,3 +73,19 @@ func searchBrowserLauncher(goos string) (browser string) { return browser } + +func normalizeNewlines(d []byte) []byte { + // from https://github.com/MichaelMure/go-term-markdown/issues/1#issuecomment-570702862 + // replace CR LF \r\n (windows) with LF \n (unix) + d = bytes.Replace(d, []byte{13, 10}, []byte{10}, -1) + // replace CF \r (mac) with LF \n (unix) + d = bytes.Replace(d, []byte{13}, []byte{10}, -1) + return d +} + +func RenderMarkdown(text string) string { + textB := []byte(text) + textB = normalizeNewlines(textB) + + return mdCompiler.Compile(string(textB)) +} From e42b0976293aa8c0a5c97d49083c4a52527fd5e8 Mon Sep 17 00:00:00 2001 From: vilmibm Date: Fri, 10 Jan 2020 14:18:37 -0600 Subject: [PATCH 2/8] inline a markdown rendering lib This commit inlines https://github.com/tj/go-termd for a few reasons: - off the shelf it relies on a broken, erroneously released version of blackfriday (a markdown parser) - based on discussion with ampinsk, there are some tweaks we'd like to make to markdown rendering beyond what the library exposes now - it's a small library (around 300 sloc) This commit only: - messes with go.mod to fix the blackfriday issues - adds an inclusion note - renames the package --- go.mod | 8 +- go.sum | 6 +- markdown/highlight.go | 36 ++++++++ markdown/markdown.go | 122 +++++++++++++++++++++++++ markdown/theme.go | 202 ++++++++++++++++++++++++++++++++++++++++++ utils/utils.go | 2 +- 6 files changed, 370 insertions(+), 6 deletions(-) create mode 100644 markdown/highlight.go create mode 100644 markdown/markdown.go create mode 100644 markdown/theme.go diff --git a/go.mod b/go.mod index 813f8538e..f97e9caeb 100644 --- a/go.mod +++ b/go.mod @@ -4,17 +4,23 @@ go 1.13 require ( github.com/AlecAivazis/survey/v2 v2.0.4 + github.com/alecthomas/chroma v0.6.8 + github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/hashicorp/go-version v1.2.0 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 + github.com/kr/text v0.1.0 github.com/mattn/go-colorable v0.1.2 github.com/mattn/go-isatty v0.0.9 github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b github.com/mitchellh/go-homedir v1.1.0 + github.com/mitchellh/go-wordwrap v1.0.0 github.com/pkg/errors v0.8.1 + github.com/russross/blackfriday/v2 v2.0.1 + github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect github.com/spf13/cobra v0.0.5 github.com/spf13/pflag v1.0.5 - github.com/tj/go-termd v0.0.1 + github.com/tj/go-css v0.0.0-20191108133013-220a796d1705 golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5 gopkg.in/yaml.v3 v3.0.0-20191010095647-fc94e3f71652 ) diff --git a/go.sum b/go.sum index 9d079db48..4164f7646 100644 --- a/go.sum +++ b/go.sum @@ -80,8 +80,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= -github.com/russross/blackfriday v2.0.0+incompatible h1:cBXrhZNUf9C+La9/YpS+UHpUT8YD6Td9ZMSU9APFcsk= -github.com/russross/blackfriday v2.0.0+incompatible/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= @@ -107,8 +107,6 @@ github.com/tj/assert v0.0.0-20190920132354-ee03d75cd160 h1:NSWpaDaurcAJY7PkL8Xt0 github.com/tj/assert v0.0.0-20190920132354-ee03d75cd160/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0= github.com/tj/go-css v0.0.0-20191108133013-220a796d1705 h1:+UA89aFRjPMqdccHd9A0HLNCRDXIoElaDoW2C1V3TzA= github.com/tj/go-css v0.0.0-20191108133013-220a796d1705/go.mod h1:e+JPLQ9wyQCgRnPenX2bo7MJoLphBHz5c1WUqaANSeA= -github.com/tj/go-termd v0.0.1 h1:NRrUrpzPj3jVlimGNMdnNOry0vYgvEkMJcJWZkKAeZI= -github.com/tj/go-termd v0.0.1/go.mod h1:qf28T7t3aasdTnAz6ehff7dfebsK+lAKK53duclZ/yQ= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= diff --git a/markdown/highlight.go b/markdown/highlight.go new file mode 100644 index 000000000..5d44744ed --- /dev/null +++ b/markdown/highlight.go @@ -0,0 +1,36 @@ +// This package is sourced from https://github.com/tj/go-termd under the terms of the MIT license. +// It was inlined to work around some dependency issues. +package markdown + +import ( + "strings" + + "github.com/alecthomas/chroma" + "github.com/alecthomas/chroma/lexers" +) + +// SyntaxHighlighter is the interface used to highlight blocks of code. +type SyntaxHighlighter interface { + Token(chroma.Token) string +} + +// highlight returns highlighted code, or the input text on error. +func highlight(source, lang string, highlight SyntaxHighlighter) string { + l := lexers.Get(lang) + if l == nil { + return source + } + + l = chroma.Coalesce(l) + + it, err := l.Tokenise(nil, source) + if err != nil { + return source + } + + var w strings.Builder + for _, t := range it.Tokens() { + w.WriteString(highlight.Token(t)) + } + return w.String() +} diff --git a/markdown/markdown.go b/markdown/markdown.go new file mode 100644 index 000000000..68e3a7c8c --- /dev/null +++ b/markdown/markdown.go @@ -0,0 +1,122 @@ +// Package markdown provides terminal markdown rendering, +// with code block syntax highlighting support. +// This package is sourced from https://github.com/tj/go-termd under the terms of the MIT license. +// It was inlined to work around some dependency issues. +package markdown + +import ( + "fmt" + "strings" + + "github.com/kr/text" + "github.com/mitchellh/go-wordwrap" + blackfriday "github.com/russross/blackfriday/v2" +) + +// Compiler is the markdown to text compiler. The zero value can be used. +type Compiler struct { + // Columns is the number of columns to wrap text, defaulting to 90. + Columns int + + // Markdown is an optional instance of a blackfriday markdown parser, + // defaulting to one with CommonExtensions enabled. + Markdown *blackfriday.Markdown + + // SyntaxHighlighter is an optional syntax highlighter for code blocks, + // using the low-level SyntaxHighlighter interface, or SyntaxTheme map. + SyntaxHighlighter + + inBlockQuote bool + inList bool +} + +// Compile returns a terminal-styled plain text representation of a markdown string. +func (c *Compiler) Compile(s string) string { + if c.Markdown == nil { + c.Markdown = blackfriday.New(blackfriday.WithExtensions(blackfriday.CommonExtensions)) + } + + if c.Columns == 0 { + c.Columns = 90 + } + + if c.SyntaxHighlighter == nil { + c.SyntaxHighlighter = SyntaxTheme{} + } + + n := c.Markdown.Parse([]byte(s)) + return c.visit(n) +} + +// visit returns a compiled node string. +func (c *Compiler) visit(n *blackfriday.Node) (s string) { + switch n.Type { + case blackfriday.Document: + s = c.visit(n.FirstChild) + case blackfriday.BlockQuote: + prev := c.inBlockQuote + c.inBlockQuote = true + s = c.visit(n.FirstChild) + s = fmt.Sprintf("\033[38;5;102m%s\033[m", s) + c.inBlockQuote = prev + case blackfriday.List: + prev := c.inList + c.inList = true + s = fmt.Sprintf("%s\n", c.visit(n.FirstChild)) + c.inList = prev + case blackfriday.Item: + s = fmt.Sprintf(" - %s", c.visit(n.FirstChild)) + case blackfriday.Paragraph: + s = c.visit(n.FirstChild) + if c.inList { + s += "\n" + } else { + s = wordwrap.WrapString(s, uint(c.Columns)) + s = text.Indent(s, " ") + s += "\n\n" + } + case blackfriday.Heading: + h := strings.Repeat("#", n.HeadingData.Level) + t := c.visit(n.FirstChild) + s = fmt.Sprintf("\033[1m%s %s\033[m\n\n", h, t) + case blackfriday.HorizontalRule: + s = fmt.Sprintf(" %s\n\n", strings.Repeat("─", c.Columns)) + case blackfriday.Emph: + s = c.visit(n.FirstChild) + if !c.inBlockQuote { + s = fmt.Sprintf("\033[3m%s\033[m", s) + } + case blackfriday.Strong: + s = c.visit(n.FirstChild) + if !c.inBlockQuote { + s = fmt.Sprintf("\033[1m%s\033[m", s) + } + case blackfriday.Link: + s = c.visit(n.FirstChild) + d := string(n.LinkData.Destination) + if s != d { + s = fmt.Sprintf("%s (%s)", s, d) + } + case blackfriday.Image: + s = string(n.LinkData.Destination) + case blackfriday.Text: + s = string(n.Literal) + case blackfriday.CodeBlock: + lang := string(n.CodeBlockData.Info) + s = string(n.Literal) + s = highlight(s, lang, c.SyntaxHighlighter) + s = fmt.Sprintf("%s\n", text.Indent(s, " ")) + case blackfriday.Code: + s = fmt.Sprintf("\033[38;5;102m`%s`\033[m", string(n.Literal)) + case blackfriday.HTMLSpan: + // ignore + default: + s = fmt.Sprintf("", n) + } + + if n.Next != nil { + s += c.visit(n.Next) + } + + return +} diff --git a/markdown/theme.go b/markdown/theme.go new file mode 100644 index 000000000..84086c924 --- /dev/null +++ b/markdown/theme.go @@ -0,0 +1,202 @@ +// This package is sourced from https://github.com/tj/go-termd under the terms of the MIT license. +// It was inlined to work around some dependency issues. +package markdown + +import ( + "fmt" + + "github.com/alecthomas/chroma" + "github.com/aybabtme/rgbterm" + "github.com/tj/go-css/csshex" +) + +// Style is the configuration used to style a particular token. +type Style struct { + Color string `json:"color"` + Background string `json:"background"` + Bold bool `json:"bold"` + Faint bool `json:"faint"` + Italic bool `json:"italic"` + Underline bool `json:"underline"` +} + +// apply returns a string with the style applied. +func (s Style) apply(v string) string { + if s.Bold { + v = escape(1, v) + } + + if s.Faint { + v = escape(2, v) + } + + if s.Italic { + v = escape(3, v) + } + + if s.Underline { + v = escape(4, v) + } + + if s.Color != "" { + v = foreground(v, s.Color) + } + + if s.Background != "" { + v = background(v, s.Background) + } + + return v +} + +// SyntaxTheme is a map of token names to style configurations. +type SyntaxTheme map[string]Style + +// Token implementation. +func (c SyntaxTheme) Token(t chroma.Token) string { + // specific + if s, ok := c.mapped(t.Type, t.Value); ok { + return s + } + + // sub-category + if s, ok := c.mapped(t.Type.SubCategory(), t.Value); ok { + return s + } + + // category + if s, ok := c.mapped(t.Type.Category(), t.Value); ok { + return s + } + + return t.Value +} + +// mapped returns a string mapped to its style, or returns the input string as-is. +func (c SyntaxTheme) mapped(t chroma.TokenType, v string) (string, bool) { + // check if the key is valid + k, ok := themeKeys[t] + if !ok { + return v, false + } + + // check if the style is mapped + s, ok := c[k] + if !ok { + return v, false + } + + return s.apply(v), true +} + +// escape returns an ansi escape sequence with the given code. +func escape(code int, s string) string { + return fmt.Sprintf("\033[%dm%s\033[m", code, s) +} + +// foreground color. +func foreground(s, color string) string { + r, g, b, ok := csshex.Parse(color) + if !ok { + return s + } + return rgbterm.FgString(s, r, g, b) +} + +// background color. +func background(s, color string) string { + r, g, b, ok := csshex.Parse(color) + if !ok { + return s + } + return rgbterm.BgString(s, r, g, b) +} + +// themeKeys is the map of token types to names. +var themeKeys = map[chroma.TokenType]string{ + chroma.Keyword: "keyword", + chroma.KeywordConstant: "keyword.constant", + chroma.KeywordDeclaration: "keyword.declaration", + chroma.KeywordNamespace: "keyword.namespace", + chroma.KeywordPseudo: "keyword.pseudo", + chroma.KeywordReserved: "keyword.reserved", + chroma.KeywordType: "keyword.type", + chroma.Name: "name", + chroma.NameAttribute: "name.attribute", + chroma.NameBuiltin: "name.builtin", + chroma.NameBuiltinPseudo: "name.builtin.pseudo", + chroma.NameClass: "name.class", + chroma.NameConstant: "name.constant", + chroma.NameDecorator: "name.decorator", + chroma.NameEntity: "name.entity", + chroma.NameException: "name.exception", + chroma.NameFunction: "name.function", + chroma.NameFunctionMagic: "name.function.magic", + chroma.NameKeyword: "name.keyword", + chroma.NameLabel: "name.label", + chroma.NameNamespace: "name.namespace", + chroma.NameOperator: "name.operator", + chroma.NameOther: "name.other", + chroma.NamePseudo: "name.pseudo", + chroma.NameProperty: "name.property", + chroma.NameTag: "name.tag", + chroma.NameVariable: "name.variable", + chroma.NameVariableAnonymous: "name.variable.anonymous", + chroma.NameVariableClass: "name.variable.class", + chroma.NameVariableGlobal: "name.variable.global", + chroma.NameVariableInstance: "name.variable.instance", + chroma.NameVariableMagic: "name.variable.magic", + chroma.Literal: "literal", + chroma.LiteralDate: "literal.date", + chroma.LiteralOther: "literal.other", + chroma.LiteralString: "literal.string", + chroma.LiteralStringAffix: "literal.string.affix", + chroma.LiteralStringAtom: "literal.string.atom", + chroma.LiteralStringBacktick: "literal.string.backtick", + chroma.LiteralStringBoolean: "literal.string.boolean", + chroma.LiteralStringChar: "literal.string.char", + chroma.LiteralStringDelimiter: "literal.string.delimiter", + chroma.LiteralStringDoc: "literal.string.doc", + chroma.LiteralStringDouble: "literal.string.double", + chroma.LiteralStringEscape: "literal.string.escape", + chroma.LiteralStringHeredoc: "literal.string.heredoc", + chroma.LiteralStringInterpol: "literal.string.interpol", + chroma.LiteralStringName: "literal.string.name", + chroma.LiteralStringOther: "literal.string.other", + chroma.LiteralStringRegex: "literal.string.regex", + chroma.LiteralStringSingle: "literal.string.single", + chroma.LiteralStringSymbol: "literal.string.symbol", + chroma.LiteralNumber: "literal.number", + chroma.LiteralNumberBin: "literal.number.bin", + chroma.LiteralNumberFloat: "literal.number.float", + chroma.LiteralNumberHex: "literal.number.hex", + chroma.LiteralNumberInteger: "literal.number.integer", + chroma.LiteralNumberIntegerLong: "literal.number.integer.long", + chroma.LiteralNumberOct: "literal.number.oct", + chroma.Operator: "operator", + chroma.OperatorWord: "operator.word", + chroma.Punctuation: "punctuation", + chroma.Comment: "comment", + chroma.CommentHashbang: "comment.hashbang", + chroma.CommentMultiline: "comment.multiline", + chroma.CommentSingle: "comment.single", + chroma.CommentSpecial: "comment.special", + chroma.CommentPreproc: "comment.preproc", + chroma.CommentPreprocFile: "comment.preproc.file", + chroma.Generic: "generic", + chroma.GenericDeleted: "generic.deleted", + chroma.GenericEmph: "generic.emph", + chroma.GenericError: "generic.error", + chroma.GenericHeading: "generic.heading", + chroma.GenericInserted: "generic.inserted", + chroma.GenericOutput: "generic.output", + chroma.GenericPrompt: "generic.prompt", + chroma.GenericStrong: "generic.strong", + chroma.GenericSubheading: "generic.subheading", + chroma.GenericTraceback: "generic.traceback", + chroma.GenericUnderline: "generic.underline", + chroma.Text: "text", + chroma.TextWhitespace: "text.whitespace", + chroma.TextSymbol: "text.symbol", + chroma.TextPunctuation: "text.punctuation", +} diff --git a/utils/utils.go b/utils/utils.go index 5daa4710d..e2bbdf709 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -7,8 +7,8 @@ import ( "os/exec" "runtime" + md "github.com/github/gh-cli/markdown" "github.com/kballard/go-shellquote" - md "github.com/tj/go-termd" ) var mdCompiler md.Compiler From f911a234c29eda1446a0df4c791a3b1d2ea8a441 Mon Sep 17 00:00:00 2001 From: vilmibm Date: Fri, 10 Jan 2020 15:12:54 -0600 Subject: [PATCH 3/8] print to proper handle --- command/issue.go | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/command/issue.go b/command/issue.go index 02c5b5adf..b5377f522 100644 --- a/command/issue.go +++ b/command/issue.go @@ -237,15 +237,17 @@ func issueView(cmd *cobra.Command, args []string) error { } meta += coloredLabels - fmt.Println(utils.Bold(issue.Title)) - fmt.Println(utils.Gray(fmt.Sprintf(meta, + out := colorableOut(cmd) + + fmt.Fprintln(out, utils.Bold(issue.Title)) + fmt.Fprintln(out, utils.Gray(fmt.Sprintf(meta, issue.Author.Login, issue.Comments.TotalCount, ))) - fmt.Println() - fmt.Println(utils.RenderMarkdown(issue.Body)) - fmt.Println() - fmt.Println(utils.Gray(fmt.Sprintf("View this issue on GitHub: %s", openURL))) + fmt.Fprintln(out) + fmt.Fprintln(out, utils.RenderMarkdown(issue.Body)) + fmt.Fprintln(out) + fmt.Fprintf(out, utils.Gray("View this issue on GitHub: %s\n"), openURL) return nil } else { fmt.Fprintf(cmd.ErrOrStderr(), "Opening %s in your browser.\n", openURL) From bbdf30c8f877c4c2f70f11878184c7296fa4c4f0 Mon Sep 17 00:00:00 2001 From: vilmibm Date: Fri, 10 Jan 2020 15:13:02 -0600 Subject: [PATCH 4/8] add tests for pr/issue previewing in terminal --- command/issue_test.go | 45 ++++++++++++++++++++++++++++++++ command/pr.go | 15 ++++++----- command/pr_test.go | 29 ++++++++++++++++++++ test/fixtures/prViewPreview.json | 24 +++++++++++++++++ 4 files changed, 107 insertions(+), 6 deletions(-) create mode 100644 test/fixtures/prViewPreview.json diff --git a/command/issue_test.go b/command/issue_test.go index 78189e4c7..93229dac2 100644 --- a/command/issue_test.go +++ b/command/issue_test.go @@ -231,6 +231,51 @@ func TestIssueView(t *testing.T) { eq(t, url, "https://github.com/OWNER/REPO/issues/123") } +func TestIssueView_preview(t *testing.T) { + initBlankContext("OWNER/REPO", "master") + http := initFakeHTTP() + + http.StubResponse(200, bytes.NewBufferString(` + { "data": { "repository": { "issue": { + "number": 123, + "body": "**bold story**", + "title": "ix of coins", + "author": { + "login": "marseilles" + }, + "labels": { + "nodes": [ + {"name": "tarot"} + ] + }, + "comments": { + "totalCount": 9 + }, + "url": "https://github.com/OWNER/REPO/issues/123" + } } } } + `)) + + output, err := RunCommand(issueViewCmd, "issue view -p 123") + if err != nil { + t.Errorf("error running command `issue view`: %v", err) + } + + eq(t, output.Stderr(), "") + + expectedLines := []*regexp.Regexp{ + regexp.MustCompile(`ix of coins`), + regexp.MustCompile(`opened by marseilles. 9 comments. \(tarot\)`), + regexp.MustCompile(`bold story`), + regexp.MustCompile(`View this issue on GitHub: https://github.com/OWNER/REPO/issues/123`), + } + for _, r := range expectedLines { + if !r.MatchString(output.String()) { + t.Errorf("output did not match regexp /%s/\n> output\n%s\n", r, output) + return + } + } +} + func TestIssueView_notFound(t *testing.T) { initBlankContext("OWNER/REPO", "master") http := initFakeHTTP() diff --git a/command/pr.go b/command/pr.go index 82bbec7b2..85ac58fa1 100644 --- a/command/pr.go +++ b/command/pr.go @@ -298,17 +298,20 @@ func prView(cmd *cobra.Command, args []string) error { meta += "s " } meta += "into %s from %s" - fmt.Println(utils.Bold(pr.Title)) - fmt.Println(utils.Gray(fmt.Sprintf(meta, + + out := colorableOut(cmd) + + fmt.Fprintln(out, utils.Bold(pr.Title)) + fmt.Fprintln(out, utils.Gray(fmt.Sprintf(meta, pr.Author.Login, pr.Commits.TotalCount, pr.BaseRefName, pr.HeadRefName, ))) - fmt.Println() - fmt.Println(utils.RenderMarkdown(pr.Body)) - fmt.Println() - fmt.Println(utils.Gray(fmt.Sprintf("View this PR on GitHub: %s", openURL))) + fmt.Fprintln(out) + fmt.Fprintln(out, utils.RenderMarkdown(pr.Body)) + fmt.Fprintln(out) + fmt.Fprintf(out, utils.Gray("View this PR on GitHub: %s\n"), openURL) return nil } else { diff --git a/command/pr_test.go b/command/pr_test.go index 360b6d852..e901b38bf 100644 --- a/command/pr_test.go +++ b/command/pr_test.go @@ -238,6 +238,35 @@ func TestPRList_filteringAssigneeLabels(t *testing.T) { } } +func TestPRView_preview(t *testing.T) { + initBlankContext("OWNER/REPO", "master") + http := initFakeHTTP() + + jsonFile, _ := os.Open("../test/fixtures/prViewPreview.json") + defer jsonFile.Close() + http.StubResponse(200, jsonFile) + + output, err := RunCommand(prViewCmd, "pr view -p 12") + if err != nil { + t.Errorf("error running command `pr view`: %v", err) + } + + eq(t, output.Stderr(), "") + + expectedLines := []*regexp.Regexp{ + regexp.MustCompile(`Blueberries are from a fork`), + regexp.MustCompile(`nobody wants to merge 12 commits into master from blueberries`), + regexp.MustCompile(`blueberries taste good`), + regexp.MustCompile(`View this PR on GitHub: https://github.com/OWNER/REPO/pull/12`), + } + for _, r := range expectedLines { + if !r.MatchString(output.String()) { + t.Errorf("output did not match regexp /%s/\n> output\n%s\n", r, output) + return + } + } +} + func TestPRView_currentBranch(t *testing.T) { initBlankContext("OWNER/REPO", "blueberries") http := initFakeHTTP() diff --git a/test/fixtures/prViewPreview.json b/test/fixtures/prViewPreview.json new file mode 100644 index 000000000..e40946774 --- /dev/null +++ b/test/fixtures/prViewPreview.json @@ -0,0 +1,24 @@ +{ + "data": { + "repository": { + "pullRequest": { + "number": 12, + "title": "Blueberries are from a fork", + "body": "**blueberries taste good**", + "url": "https://github.com/OWNER/REPO/pull/12", + "author": { + "login": "nobody" + }, + "commits": { + "totalCount": 12 + }, + "baseRefName": "master", + "headRefName": "blueberries", + "headRepositoryOwner": { + "login": "hubot" + }, + "isCrossRepository": true + } + } + } +} From bc1ab20cdfc1ef19f59e360c04990e0a089e4af3 Mon Sep 17 00:00:00 2001 From: vilmibm Date: Fri, 10 Jan 2020 15:35:41 -0600 Subject: [PATCH 5/8] turns out this has frightening state --- utils/utils.go | 35 +++++++++++++++-------------------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/utils/utils.go b/utils/utils.go index e2bbdf709..0183731c6 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -11,26 +11,6 @@ import ( "github.com/kballard/go-shellquote" ) -var mdCompiler md.Compiler - -func init() { - mdCompiler = md.Compiler{ - Columns: 100, - SyntaxHighlighter: md.SyntaxTheme{ - "keyword": md.Style{Color: "#9196ed"}, - "comment": md.Style{ - Color: "#c0c0c2", - }, - "literal": md.Style{ - Color: "#aaedf7", - }, - "name": md.Style{ - Color: "#fe8eb5", - }, - }, - } -} - func OpenInBrowser(url string) error { browser := os.Getenv("BROWSER") if browser == "" { @@ -86,6 +66,21 @@ func normalizeNewlines(d []byte) []byte { func RenderMarkdown(text string) string { textB := []byte(text) textB = normalizeNewlines(textB) + mdCompiler := md.Compiler{ + Columns: 100, + SyntaxHighlighter: md.SyntaxTheme{ + "keyword": md.Style{Color: "#9196ed"}, + "comment": md.Style{ + Color: "#c0c0c2", + }, + "literal": md.Style{ + Color: "#aaedf7", + }, + "name": md.Style{ + Color: "#fe8eb5", + }, + }, + } return mdCompiler.Compile(string(textB)) } From 5bc6d220c4ce678b48dacc847901fbb0bc5df790 Mon Sep 17 00:00:00 2001 From: vilmibm Date: Mon, 13 Jan 2020 15:28:13 -0600 Subject: [PATCH 6/8] review feedback --- command/issue.go | 44 ++++++++++++++++++-------------------- command/issue_test.go | 2 +- command/pr.go | 45 ++++++++++++++++++++------------------- command/pr_test.go | 34 +++++++++++++++++++++++++++++ test/fixtures/prView.json | 16 ++++++++++++++ utils/utils.go | 9 ++++++++ 6 files changed, 104 insertions(+), 46 deletions(-) diff --git a/command/issue.go b/command/issue.go index b5377f522..d6b06ad29 100644 --- a/command/issue.go +++ b/command/issue.go @@ -35,7 +35,7 @@ func init() { issueListCmd.Flags().StringP("state", "s", "", "Filter by state: {open|closed|all}") issueListCmd.Flags().IntP("limit", "L", 30, "Maximum number of issues to fetch") - issueViewCmd.Flags().BoolP("preview", "p", false, "Preview PR in termianl") + issueViewCmd.Flags().BoolP("preview", "p", false, "Preview PR in terminal") } var issueCmd = &cobra.Command{ @@ -225,29 +225,8 @@ func issueView(cmd *cobra.Command, args []string) error { } if preview { - coloredLabels := labelList(*issue) - if coloredLabels != "" { - coloredLabels = utils.Gray(fmt.Sprintf(" (%s)", coloredLabels)) - } - meta := "opened by %s. %d comment" - if issue.Comments.TotalCount != 1 { - meta += "s." - } else { - meta += "." - } - meta += coloredLabels - out := colorableOut(cmd) - - fmt.Fprintln(out, utils.Bold(issue.Title)) - fmt.Fprintln(out, utils.Gray(fmt.Sprintf(meta, - issue.Author.Login, - issue.Comments.TotalCount, - ))) - fmt.Fprintln(out) - fmt.Fprintln(out, utils.RenderMarkdown(issue.Body)) - fmt.Fprintln(out) - fmt.Fprintf(out, utils.Gray("View this issue on GitHub: %s\n"), openURL) + printIssuePreview(out, issue) return nil } else { fmt.Fprintf(cmd.ErrOrStderr(), "Opening %s in your browser.\n", openURL) @@ -256,6 +235,25 @@ func issueView(cmd *cobra.Command, args []string) error { } +func printIssuePreview(out io.Writer, issue *api.Issue) { + coloredLabels := labelList(*issue) + if coloredLabels != "" { + coloredLabels = utils.Gray(fmt.Sprintf("(%s)", coloredLabels)) + } + + fmt.Fprintln(out, utils.Bold(issue.Title)) + fmt.Fprintln(out, utils.Gray(fmt.Sprintf( + "opened by %s. %s. %s", + issue.Author.Login, + utils.Pluralize(issue.Comments.TotalCount, "comment"), + coloredLabels, + ))) + fmt.Fprintln(out) + fmt.Fprintln(out, utils.RenderMarkdown(issue.Body)) + fmt.Fprintln(out) + fmt.Fprintf(out, utils.Gray("View this issue on GitHub: %s\n"), issue.URL) +} + var issueURLRE = regexp.MustCompile(`^https://github\.com/([^/]+)/([^/]+)/issues/(\d+)`) func issueFromArg(apiClient *api.Client, baseRepo context.GitHubRepository, arg string) (*api.Issue, error) { diff --git a/command/issue_test.go b/command/issue_test.go index 93229dac2..e9f616f68 100644 --- a/command/issue_test.go +++ b/command/issue_test.go @@ -264,7 +264,7 @@ func TestIssueView_preview(t *testing.T) { expectedLines := []*regexp.Regexp{ regexp.MustCompile(`ix of coins`), - regexp.MustCompile(`opened by marseilles. 9 comments. \(tarot\)`), + regexp.MustCompile(`opened by marseilles. 9 comments. \(tarot\)`), regexp.MustCompile(`bold story`), regexp.MustCompile(`View this issue on GitHub: https://github.com/OWNER/REPO/issues/123`), } diff --git a/command/pr.go b/command/pr.go index 85ac58fa1..328ebb134 100644 --- a/command/pr.go +++ b/command/pr.go @@ -32,7 +32,7 @@ func init() { prListCmd.Flags().StringSliceP("label", "l", nil, "Filter by label") prListCmd.Flags().StringP("assignee", "a", "", "Filter by assignee") - prViewCmd.Flags().BoolP("preview", "p", false, "Preview PR in termianl") + prViewCmd.Flags().BoolP("preview", "p", false, "Preview PR in terminal") } var prCmd = &cobra.Command{ @@ -276,6 +276,12 @@ func prView(cmd *cobra.Command, args []string) error { if prNumber > 0 { openURL = fmt.Sprintf("https://github.com/%s/%s/pull/%d", baseRepo.RepoOwner(), baseRepo.RepoName(), prNumber) + if preview { + pr, err = api.PullRequestByNumber(apiClient, baseRepo, prNumber) + if err != nil { + return err + } + } } else { pr, err = api.PullRequestForBranch(apiClient, baseRepo, branchWithOwner) if err != nil { @@ -291,28 +297,8 @@ func prView(cmd *cobra.Command, args []string) error { } if preview { - meta := "%s wants to merge %d commit" - if pr.Commits.TotalCount == 1 { - meta += " " - } else { - meta += "s " - } - meta += "into %s from %s" - out := colorableOut(cmd) - - fmt.Fprintln(out, utils.Bold(pr.Title)) - fmt.Fprintln(out, utils.Gray(fmt.Sprintf(meta, - pr.Author.Login, - pr.Commits.TotalCount, - pr.BaseRefName, - pr.HeadRefName, - ))) - fmt.Fprintln(out) - fmt.Fprintln(out, utils.RenderMarkdown(pr.Body)) - fmt.Fprintln(out) - fmt.Fprintf(out, utils.Gray("View this PR on GitHub: %s\n"), openURL) - + printPrPreview(out, pr) return nil } else { fmt.Fprintf(cmd.ErrOrStderr(), "Opening %s in your browser.\n", openURL) @@ -320,6 +306,21 @@ func prView(cmd *cobra.Command, args []string) error { } } +func printPrPreview(out io.Writer, pr *api.PullRequest) { + fmt.Fprintln(out, utils.Bold(pr.Title)) + fmt.Fprintln(out, utils.Gray(fmt.Sprintf( + "%s wants to merge %s into %s from %s", + pr.Author.Login, + utils.Pluralize(pr.Commits.TotalCount, "commit"), + pr.BaseRefName, + pr.HeadRefName, + ))) + fmt.Fprintln(out) + fmt.Fprintln(out, utils.RenderMarkdown(pr.Body)) + fmt.Fprintln(out) + fmt.Fprintf(out, utils.Gray("View this PR on GitHub: %s\n"), pr.URL) +} + var prURLRE = regexp.MustCompile(`^https://github\.com/([^/]+)/([^/]+)/pull/(\d+)`) func prFromArg(apiClient *api.Client, baseRepo context.GitHubRepository, arg string) (*api.PullRequest, error) { diff --git a/command/pr_test.go b/command/pr_test.go index e901b38bf..f6bc733b1 100644 --- a/command/pr_test.go +++ b/command/pr_test.go @@ -267,6 +267,40 @@ func TestPRView_preview(t *testing.T) { } } +func TestPRView_previewCurrentBranch(t *testing.T) { + initBlankContext("OWNER/REPO", "blueberries") + http := initFakeHTTP() + + jsonFile, _ := os.Open("../test/fixtures/prView.json") + defer jsonFile.Close() + http.StubResponse(200, jsonFile) + + restoreCmd := utils.SetPrepareCmd(func(cmd *exec.Cmd) utils.Runnable { + return &outputStub{} + }) + defer restoreCmd() + + output, err := RunCommand(prViewCmd, "pr view -p") + if err != nil { + t.Errorf("error running command `pr view`: %v", err) + } + + eq(t, output.Stderr(), "") + + expectedLines := []*regexp.Regexp{ + regexp.MustCompile(`Blueberries are a good fruit`), + regexp.MustCompile(`nobody wants to merge 8 commits into master from blueberries`), + regexp.MustCompile(`blueberries taste good`), + regexp.MustCompile(`View this PR on GitHub: https://github.com/OWNER/REPO/pull/10`), + } + for _, r := range expectedLines { + if !r.MatchString(output.String()) { + t.Errorf("output did not match regexp /%s/\n> output\n%s\n", r, output) + return + } + } +} + func TestPRView_currentBranch(t *testing.T) { initBlankContext("OWNER/REPO", "blueberries") http := initFakeHTTP() diff --git a/test/fixtures/prView.json b/test/fixtures/prView.json index c8ae23ce6..02277d7a5 100644 --- a/test/fixtures/prView.json +++ b/test/fixtures/prView.json @@ -6,21 +6,37 @@ { "number": 12, "title": "Blueberries are from a fork", + "body": "yeah", "url": "https://github.com/OWNER/REPO/pull/12", "headRefName": "blueberries", + "baseRefName": "master", "headRepositoryOwner": { "login": "hubot" }, + "commits": { + "totalCount": 12 + }, + "author": { + "login": "nobody" + }, "isCrossRepository": true }, { "number": 10, "title": "Blueberries are a good fruit", + "body": "**blueberries taste good**", "url": "https://github.com/OWNER/REPO/pull/10", + "baseRefName": "master", "headRefName": "blueberries", + "author": { + "login": "nobody" + }, "headRepositoryOwner": { "login": "OWNER" }, + "commits": { + "totalCount": 8 + }, "isCrossRepository": false } ] diff --git a/utils/utils.go b/utils/utils.go index 0183731c6..49d9cae2c 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -3,6 +3,7 @@ package utils import ( "bytes" "errors" + "fmt" "os" "os/exec" "runtime" @@ -84,3 +85,11 @@ func RenderMarkdown(text string) string { return mdCompiler.Compile(string(textB)) } + +func Pluralize(num int, thing string) string { + if num == 1 { + return fmt.Sprintf("%d %s", num, thing) + } else { + return fmt.Sprintf("%d %ss", num, thing) + } +} From 4592aaf63de9451416d31210e58d3542ed843025 Mon Sep 17 00:00:00 2001 From: vilmibm Date: Mon, 13 Jan 2020 15:36:43 -0600 Subject: [PATCH 7/8] clarify newline normalization --- utils/utils.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/utils/utils.go b/utils/utils.go index 49d9cae2c..2fc878469 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -56,11 +56,8 @@ func searchBrowserLauncher(goos string) (browser string) { } func normalizeNewlines(d []byte) []byte { - // from https://github.com/MichaelMure/go-term-markdown/issues/1#issuecomment-570702862 - // replace CR LF \r\n (windows) with LF \n (unix) - d = bytes.Replace(d, []byte{13, 10}, []byte{10}, -1) - // replace CF \r (mac) with LF \n (unix) - d = bytes.Replace(d, []byte{13}, []byte{10}, -1) + d = bytes.Replace(d, []byte("\r\n"), []byte("\n"), -1) + d = bytes.Replace(d, []byte("\r"), []byte("\n"), -1) return d } From daf02ff3ec653096266d871752dc7d2866719759 Mon Sep 17 00:00:00 2001 From: vilmibm Date: Mon, 13 Jan 2020 16:10:22 -0600 Subject: [PATCH 8/8] de-inline go-termd --- go.mod | 8 +- go.sum | 2 + markdown/highlight.go | 36 -------- markdown/markdown.go | 122 ------------------------- markdown/theme.go | 202 ------------------------------------------ utils/utils.go | 2 +- 6 files changed, 4 insertions(+), 368 deletions(-) delete mode 100644 markdown/highlight.go delete mode 100644 markdown/markdown.go delete mode 100644 markdown/theme.go diff --git a/go.mod b/go.mod index f97e9caeb..df49a1664 100644 --- a/go.mod +++ b/go.mod @@ -4,23 +4,17 @@ go 1.13 require ( github.com/AlecAivazis/survey/v2 v2.0.4 - github.com/alecthomas/chroma v0.6.8 - github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/hashicorp/go-version v1.2.0 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 - github.com/kr/text v0.1.0 github.com/mattn/go-colorable v0.1.2 github.com/mattn/go-isatty v0.0.9 github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b github.com/mitchellh/go-homedir v1.1.0 - github.com/mitchellh/go-wordwrap v1.0.0 github.com/pkg/errors v0.8.1 - github.com/russross/blackfriday/v2 v2.0.1 - github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect github.com/spf13/cobra v0.0.5 github.com/spf13/pflag v1.0.5 - github.com/tj/go-css v0.0.0-20191108133013-220a796d1705 + github.com/vilmibm/go-termd v0.0.4 golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5 gopkg.in/yaml.v3 v3.0.0-20191010095647-fc94e3f71652 ) diff --git a/go.sum b/go.sum index 4164f7646..79806902b 100644 --- a/go.sum +++ b/go.sum @@ -110,6 +110,8 @@ github.com/tj/go-css v0.0.0-20191108133013-220a796d1705/go.mod h1:e+JPLQ9wyQCgRn github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= +github.com/vilmibm/go-termd v0.0.4 h1:uCmDUZ3qZUblTN/D5Hvl+g1rTJj/HW746JQFWidqAyk= +github.com/vilmibm/go-termd v0.0.4/go.mod h1:ys+dRO6wlM3el0vPJmYBkhOPPozViBgDXHOEn1x5Vsc= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 h1:mKdxBk7AujPs8kU4m80U72y/zjbZ3UcXC7dClwKbUI0= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= diff --git a/markdown/highlight.go b/markdown/highlight.go deleted file mode 100644 index 5d44744ed..000000000 --- a/markdown/highlight.go +++ /dev/null @@ -1,36 +0,0 @@ -// This package is sourced from https://github.com/tj/go-termd under the terms of the MIT license. -// It was inlined to work around some dependency issues. -package markdown - -import ( - "strings" - - "github.com/alecthomas/chroma" - "github.com/alecthomas/chroma/lexers" -) - -// SyntaxHighlighter is the interface used to highlight blocks of code. -type SyntaxHighlighter interface { - Token(chroma.Token) string -} - -// highlight returns highlighted code, or the input text on error. -func highlight(source, lang string, highlight SyntaxHighlighter) string { - l := lexers.Get(lang) - if l == nil { - return source - } - - l = chroma.Coalesce(l) - - it, err := l.Tokenise(nil, source) - if err != nil { - return source - } - - var w strings.Builder - for _, t := range it.Tokens() { - w.WriteString(highlight.Token(t)) - } - return w.String() -} diff --git a/markdown/markdown.go b/markdown/markdown.go deleted file mode 100644 index 68e3a7c8c..000000000 --- a/markdown/markdown.go +++ /dev/null @@ -1,122 +0,0 @@ -// Package markdown provides terminal markdown rendering, -// with code block syntax highlighting support. -// This package is sourced from https://github.com/tj/go-termd under the terms of the MIT license. -// It was inlined to work around some dependency issues. -package markdown - -import ( - "fmt" - "strings" - - "github.com/kr/text" - "github.com/mitchellh/go-wordwrap" - blackfriday "github.com/russross/blackfriday/v2" -) - -// Compiler is the markdown to text compiler. The zero value can be used. -type Compiler struct { - // Columns is the number of columns to wrap text, defaulting to 90. - Columns int - - // Markdown is an optional instance of a blackfriday markdown parser, - // defaulting to one with CommonExtensions enabled. - Markdown *blackfriday.Markdown - - // SyntaxHighlighter is an optional syntax highlighter for code blocks, - // using the low-level SyntaxHighlighter interface, or SyntaxTheme map. - SyntaxHighlighter - - inBlockQuote bool - inList bool -} - -// Compile returns a terminal-styled plain text representation of a markdown string. -func (c *Compiler) Compile(s string) string { - if c.Markdown == nil { - c.Markdown = blackfriday.New(blackfriday.WithExtensions(blackfriday.CommonExtensions)) - } - - if c.Columns == 0 { - c.Columns = 90 - } - - if c.SyntaxHighlighter == nil { - c.SyntaxHighlighter = SyntaxTheme{} - } - - n := c.Markdown.Parse([]byte(s)) - return c.visit(n) -} - -// visit returns a compiled node string. -func (c *Compiler) visit(n *blackfriday.Node) (s string) { - switch n.Type { - case blackfriday.Document: - s = c.visit(n.FirstChild) - case blackfriday.BlockQuote: - prev := c.inBlockQuote - c.inBlockQuote = true - s = c.visit(n.FirstChild) - s = fmt.Sprintf("\033[38;5;102m%s\033[m", s) - c.inBlockQuote = prev - case blackfriday.List: - prev := c.inList - c.inList = true - s = fmt.Sprintf("%s\n", c.visit(n.FirstChild)) - c.inList = prev - case blackfriday.Item: - s = fmt.Sprintf(" - %s", c.visit(n.FirstChild)) - case blackfriday.Paragraph: - s = c.visit(n.FirstChild) - if c.inList { - s += "\n" - } else { - s = wordwrap.WrapString(s, uint(c.Columns)) - s = text.Indent(s, " ") - s += "\n\n" - } - case blackfriday.Heading: - h := strings.Repeat("#", n.HeadingData.Level) - t := c.visit(n.FirstChild) - s = fmt.Sprintf("\033[1m%s %s\033[m\n\n", h, t) - case blackfriday.HorizontalRule: - s = fmt.Sprintf(" %s\n\n", strings.Repeat("─", c.Columns)) - case blackfriday.Emph: - s = c.visit(n.FirstChild) - if !c.inBlockQuote { - s = fmt.Sprintf("\033[3m%s\033[m", s) - } - case blackfriday.Strong: - s = c.visit(n.FirstChild) - if !c.inBlockQuote { - s = fmt.Sprintf("\033[1m%s\033[m", s) - } - case blackfriday.Link: - s = c.visit(n.FirstChild) - d := string(n.LinkData.Destination) - if s != d { - s = fmt.Sprintf("%s (%s)", s, d) - } - case blackfriday.Image: - s = string(n.LinkData.Destination) - case blackfriday.Text: - s = string(n.Literal) - case blackfriday.CodeBlock: - lang := string(n.CodeBlockData.Info) - s = string(n.Literal) - s = highlight(s, lang, c.SyntaxHighlighter) - s = fmt.Sprintf("%s\n", text.Indent(s, " ")) - case blackfriday.Code: - s = fmt.Sprintf("\033[38;5;102m`%s`\033[m", string(n.Literal)) - case blackfriday.HTMLSpan: - // ignore - default: - s = fmt.Sprintf("", n) - } - - if n.Next != nil { - s += c.visit(n.Next) - } - - return -} diff --git a/markdown/theme.go b/markdown/theme.go deleted file mode 100644 index 84086c924..000000000 --- a/markdown/theme.go +++ /dev/null @@ -1,202 +0,0 @@ -// This package is sourced from https://github.com/tj/go-termd under the terms of the MIT license. -// It was inlined to work around some dependency issues. -package markdown - -import ( - "fmt" - - "github.com/alecthomas/chroma" - "github.com/aybabtme/rgbterm" - "github.com/tj/go-css/csshex" -) - -// Style is the configuration used to style a particular token. -type Style struct { - Color string `json:"color"` - Background string `json:"background"` - Bold bool `json:"bold"` - Faint bool `json:"faint"` - Italic bool `json:"italic"` - Underline bool `json:"underline"` -} - -// apply returns a string with the style applied. -func (s Style) apply(v string) string { - if s.Bold { - v = escape(1, v) - } - - if s.Faint { - v = escape(2, v) - } - - if s.Italic { - v = escape(3, v) - } - - if s.Underline { - v = escape(4, v) - } - - if s.Color != "" { - v = foreground(v, s.Color) - } - - if s.Background != "" { - v = background(v, s.Background) - } - - return v -} - -// SyntaxTheme is a map of token names to style configurations. -type SyntaxTheme map[string]Style - -// Token implementation. -func (c SyntaxTheme) Token(t chroma.Token) string { - // specific - if s, ok := c.mapped(t.Type, t.Value); ok { - return s - } - - // sub-category - if s, ok := c.mapped(t.Type.SubCategory(), t.Value); ok { - return s - } - - // category - if s, ok := c.mapped(t.Type.Category(), t.Value); ok { - return s - } - - return t.Value -} - -// mapped returns a string mapped to its style, or returns the input string as-is. -func (c SyntaxTheme) mapped(t chroma.TokenType, v string) (string, bool) { - // check if the key is valid - k, ok := themeKeys[t] - if !ok { - return v, false - } - - // check if the style is mapped - s, ok := c[k] - if !ok { - return v, false - } - - return s.apply(v), true -} - -// escape returns an ansi escape sequence with the given code. -func escape(code int, s string) string { - return fmt.Sprintf("\033[%dm%s\033[m", code, s) -} - -// foreground color. -func foreground(s, color string) string { - r, g, b, ok := csshex.Parse(color) - if !ok { - return s - } - return rgbterm.FgString(s, r, g, b) -} - -// background color. -func background(s, color string) string { - r, g, b, ok := csshex.Parse(color) - if !ok { - return s - } - return rgbterm.BgString(s, r, g, b) -} - -// themeKeys is the map of token types to names. -var themeKeys = map[chroma.TokenType]string{ - chroma.Keyword: "keyword", - chroma.KeywordConstant: "keyword.constant", - chroma.KeywordDeclaration: "keyword.declaration", - chroma.KeywordNamespace: "keyword.namespace", - chroma.KeywordPseudo: "keyword.pseudo", - chroma.KeywordReserved: "keyword.reserved", - chroma.KeywordType: "keyword.type", - chroma.Name: "name", - chroma.NameAttribute: "name.attribute", - chroma.NameBuiltin: "name.builtin", - chroma.NameBuiltinPseudo: "name.builtin.pseudo", - chroma.NameClass: "name.class", - chroma.NameConstant: "name.constant", - chroma.NameDecorator: "name.decorator", - chroma.NameEntity: "name.entity", - chroma.NameException: "name.exception", - chroma.NameFunction: "name.function", - chroma.NameFunctionMagic: "name.function.magic", - chroma.NameKeyword: "name.keyword", - chroma.NameLabel: "name.label", - chroma.NameNamespace: "name.namespace", - chroma.NameOperator: "name.operator", - chroma.NameOther: "name.other", - chroma.NamePseudo: "name.pseudo", - chroma.NameProperty: "name.property", - chroma.NameTag: "name.tag", - chroma.NameVariable: "name.variable", - chroma.NameVariableAnonymous: "name.variable.anonymous", - chroma.NameVariableClass: "name.variable.class", - chroma.NameVariableGlobal: "name.variable.global", - chroma.NameVariableInstance: "name.variable.instance", - chroma.NameVariableMagic: "name.variable.magic", - chroma.Literal: "literal", - chroma.LiteralDate: "literal.date", - chroma.LiteralOther: "literal.other", - chroma.LiteralString: "literal.string", - chroma.LiteralStringAffix: "literal.string.affix", - chroma.LiteralStringAtom: "literal.string.atom", - chroma.LiteralStringBacktick: "literal.string.backtick", - chroma.LiteralStringBoolean: "literal.string.boolean", - chroma.LiteralStringChar: "literal.string.char", - chroma.LiteralStringDelimiter: "literal.string.delimiter", - chroma.LiteralStringDoc: "literal.string.doc", - chroma.LiteralStringDouble: "literal.string.double", - chroma.LiteralStringEscape: "literal.string.escape", - chroma.LiteralStringHeredoc: "literal.string.heredoc", - chroma.LiteralStringInterpol: "literal.string.interpol", - chroma.LiteralStringName: "literal.string.name", - chroma.LiteralStringOther: "literal.string.other", - chroma.LiteralStringRegex: "literal.string.regex", - chroma.LiteralStringSingle: "literal.string.single", - chroma.LiteralStringSymbol: "literal.string.symbol", - chroma.LiteralNumber: "literal.number", - chroma.LiteralNumberBin: "literal.number.bin", - chroma.LiteralNumberFloat: "literal.number.float", - chroma.LiteralNumberHex: "literal.number.hex", - chroma.LiteralNumberInteger: "literal.number.integer", - chroma.LiteralNumberIntegerLong: "literal.number.integer.long", - chroma.LiteralNumberOct: "literal.number.oct", - chroma.Operator: "operator", - chroma.OperatorWord: "operator.word", - chroma.Punctuation: "punctuation", - chroma.Comment: "comment", - chroma.CommentHashbang: "comment.hashbang", - chroma.CommentMultiline: "comment.multiline", - chroma.CommentSingle: "comment.single", - chroma.CommentSpecial: "comment.special", - chroma.CommentPreproc: "comment.preproc", - chroma.CommentPreprocFile: "comment.preproc.file", - chroma.Generic: "generic", - chroma.GenericDeleted: "generic.deleted", - chroma.GenericEmph: "generic.emph", - chroma.GenericError: "generic.error", - chroma.GenericHeading: "generic.heading", - chroma.GenericInserted: "generic.inserted", - chroma.GenericOutput: "generic.output", - chroma.GenericPrompt: "generic.prompt", - chroma.GenericStrong: "generic.strong", - chroma.GenericSubheading: "generic.subheading", - chroma.GenericTraceback: "generic.traceback", - chroma.GenericUnderline: "generic.underline", - chroma.Text: "text", - chroma.TextWhitespace: "text.whitespace", - chroma.TextSymbol: "text.symbol", - chroma.TextPunctuation: "text.punctuation", -} diff --git a/utils/utils.go b/utils/utils.go index 2fc878469..2c8c5ac0a 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -8,8 +8,8 @@ import ( "os/exec" "runtime" - md "github.com/github/gh-cli/markdown" "github.com/kballard/go-shellquote" + md "github.com/vilmibm/go-termd" ) func OpenInBrowser(url string) error {