mirror of https://gitlab.com/gitlab-org/cli.git
233 lines
5.2 KiB
Go
233 lines
5.2 KiB
Go
package utils
|
|
|
|
import (
|
|
"fmt"
|
|
"net/url"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/charmbracelet/glamour"
|
|
"gitlab.com/gitlab-org/cli/internal/run"
|
|
"gitlab.com/gitlab-org/cli/pkg/browser"
|
|
)
|
|
|
|
type MarkdownRenderOpts []glamour.TermRendererOption
|
|
|
|
// OpenInBrowser opens the url in a web browser based on OS and $BROWSER environment variable
|
|
func OpenInBrowser(url, browserType string) error {
|
|
browseCmd, err := browser.Command(url, browserType)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return run.PrepareCmd(browseCmd).Run()
|
|
}
|
|
|
|
func SanitizePathName(path string) string {
|
|
if !strings.HasPrefix(path, "/") {
|
|
// Prefix the path with "/" ensures that filepath.Clean removes all `/..`
|
|
// See rule 4 of filepath.Clean for more information: https://pkg.go.dev/path/filepath#Clean
|
|
path = "/" + path
|
|
}
|
|
return filepath.Clean(path)
|
|
}
|
|
|
|
func RenderMarkdown(text, glamourStyle string) (string, error) {
|
|
opts := MarkdownRenderOpts{
|
|
glamour.WithStylePath(getStyle(glamourStyle)),
|
|
}
|
|
|
|
return renderMarkdown(text, opts)
|
|
}
|
|
|
|
func RenderMarkdownWithoutIndentations(text, glamourStyle string) (string, error) {
|
|
opts := MarkdownRenderOpts{
|
|
glamour.WithStylePath(getStyle(glamourStyle)),
|
|
markdownWithoutIndentation(),
|
|
}
|
|
|
|
return renderMarkdown(text, opts)
|
|
}
|
|
|
|
func renderMarkdown(text string, opts MarkdownRenderOpts) (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")
|
|
|
|
tr, err := glamour.NewTermRenderer(opts...)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return tr.Render(text)
|
|
}
|
|
|
|
func markdownWithoutIndentation() glamour.TermRendererOption {
|
|
overrides := []byte(`
|
|
{
|
|
"document": {
|
|
"margin": 0
|
|
},
|
|
"code_block": {
|
|
"margin": 0
|
|
}
|
|
}`)
|
|
|
|
return glamour.WithStylesFromJSONBytes(overrides)
|
|
}
|
|
|
|
func getStyle(glamourStyle string) string {
|
|
if glamourStyle == "" || glamourStyle == "none" {
|
|
return "notty"
|
|
}
|
|
return glamourStyle
|
|
}
|
|
|
|
func Pluralize(num int, thing string) string {
|
|
if num == 1 {
|
|
return fmt.Sprintf("%d %s", num, thing)
|
|
}
|
|
return fmt.Sprintf("%d %ss", num, thing)
|
|
}
|
|
|
|
func fmtDuration(amount int, unit string) string {
|
|
return fmt.Sprintf("about %s ago", Pluralize(amount, unit))
|
|
}
|
|
|
|
func PrettyTimeAgo(ago time.Duration) string {
|
|
if ago < time.Minute {
|
|
return "less than a minute ago"
|
|
}
|
|
if ago < time.Hour {
|
|
return fmtDuration(int(ago.Minutes()), "minute")
|
|
}
|
|
if ago < 24*time.Hour {
|
|
return fmtDuration(int(ago.Hours()), "hour")
|
|
}
|
|
if ago < 30*24*time.Hour {
|
|
return fmtDuration(int(ago.Hours())/24, "day")
|
|
}
|
|
if ago < 365*24*time.Hour {
|
|
return fmtDuration(int(ago.Hours())/24/30, "month")
|
|
}
|
|
|
|
return fmtDuration(int(ago.Hours()/24/365), "year")
|
|
}
|
|
|
|
func TimeToPrettyTimeAgo(d time.Time) string {
|
|
now := time.Now()
|
|
ago := now.Sub(d)
|
|
return PrettyTimeAgo(ago)
|
|
}
|
|
|
|
func FmtDuration(d time.Duration) string {
|
|
d = d.Round(time.Second)
|
|
m := d / time.Minute
|
|
d -= m * time.Minute
|
|
s := d / time.Second
|
|
return fmt.Sprintf("%02dm %02ds", m, s)
|
|
}
|
|
|
|
func Humanize(s string) string {
|
|
// Replaces - and _ with spaces.
|
|
replace := "_-"
|
|
h := func(r rune) rune {
|
|
if strings.ContainsRune(replace, r) {
|
|
return ' '
|
|
}
|
|
return r
|
|
}
|
|
|
|
return strings.Map(h, s)
|
|
}
|
|
|
|
func DisplayURL(urlStr string) string {
|
|
u, err := url.Parse(urlStr)
|
|
if err != nil {
|
|
return urlStr
|
|
}
|
|
return u.Hostname() + u.Path
|
|
}
|
|
|
|
// PresentInStringSlice take a Hay (Slice of Strings) and a Needle (string)
|
|
// and returns true based on whether or not the Needle is present in the hay.
|
|
func PresentInStringSlice(hay []string, needle string) bool {
|
|
for x := range hay {
|
|
if hay[x] == needle {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// PresentInIntSlice take a Hay (Slice of Ints) and a Needle (int)
|
|
// and returns true based on whether or not the Needle is present in the hay.
|
|
func PresentInIntSlice(hay []int, needle int) bool {
|
|
for x := range hay {
|
|
if hay[x] == needle {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// CommonElementsInStringSlice takes 2 Slices of Strings and returns a Third Slice
|
|
// that is the common elements between the first 2 Slices.
|
|
func CommonElementsInStringSlice(s1 []string, s2 []string) (arr []string) {
|
|
hash := make(map[string]bool)
|
|
for x := range s1 {
|
|
hash[s1[x]] = true
|
|
}
|
|
for i := range s2 {
|
|
if hash[s2[i]] {
|
|
arr = append(arr, s2[i])
|
|
}
|
|
}
|
|
return arr
|
|
}
|
|
|
|
// isValidUrl tests a string to determine if it is a well-structured url or not.
|
|
func IsValidURL(toTest string) bool {
|
|
_, err := url.ParseRequestURI(toTest)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
u, err := url.Parse(toTest)
|
|
if err != nil || u.Scheme == "" || u.Host == "" {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func ByteToHumanReadableFormat(b int) string {
|
|
const unit = 1000
|
|
if b < unit {
|
|
return fmt.Sprintf("%dB", b)
|
|
}
|
|
div, exp := unit, 0
|
|
for n := b / unit; n >= unit; n /= unit {
|
|
div *= unit
|
|
exp++
|
|
}
|
|
return fmt.Sprintf("%.1f%cB",
|
|
float64(b)/float64(div), "kMGTPE"[exp])
|
|
}
|
|
|
|
// Map transfers the elements of its first argument using the result of the second fn(e)
|
|
func Map[T1, T2 any](elems []T1, fn func(T1) T2) []T2 {
|
|
r := make([]T2, len(elems))
|
|
|
|
for i, v := range elems {
|
|
r[i] = fn(v)
|
|
}
|
|
|
|
return r
|
|
}
|
|
|
|
// Ptr takes any value and returns a pointer to that value
|
|
func Ptr[T any](v T) *T {
|
|
return &v
|
|
}
|