package main import ( "errors" "flag" "fmt" "io/fs" "math" "os" "path/filepath" "regexp" "sort" "strconv" "strings" "time" "github.com/fatih/color" "github.com/peterbourgon/ff/v3" ) var ( Version = "develop" resultmap = map[string]string{ "y": "✔", "n": "✘", "a": "◒", "u": "‽", "i": "ⓘ", "w": "⚠", } supernums = map[int]string{ 0: "⁰", 1: "¹", 2: "²", 3: "³", 4: "⁴", 5: "⁵", 6: "⁶", 7: "⁷", 8: "⁸", 9: "⁹", } refresh = time.Hour * 24 * 30 ) func main() { fs := flag.NewFlagSet("caniuse", flag.ContinueOnError) versionFlag := fs.Bool("version", false, "Show the CLI version and exit") fs.BoolVar(versionFlag, "v", *versionFlag, "--version") if err := ff.Parse(fs, os.Args[1:], ff.WithEnvVarPrefix("CANIUSE"), ); err != nil { fmt.Println(err) return } if *versionFlag { fmt.Printf("caniuse %s", Version) return } if fs.NArg() != 1 { fmt.Println("caniuse requires one argument") return } data, err := loadData() if err != nil { fmt.Printf("could not load data: %v", err) } query := fs.Arg(0) // Find a single feature if feat, ok := data.Data[query]; ok { fmt.Println(formatFeat(feat, data, false)) return } // Find multiple features features := search(query, data) switch len(features) { case 0: fmt.Printf("%q not found", query) case 1: fmt.Println(formatFeat(data.Data[features[0]], data, false)) default: for _, feat := range features { fmt.Println(formatFeat(data.Data[feat], data, true)) } } } var clumpRe = regexp.MustCompile(`\W*`) func search(query string, data Data) []string { var results []string query = strings.ToLower(query) for key, dat := range data.Data { matcher := clumpRe.ReplaceAllString(strings.ToLower(key+dat.Title+dat.Description+dat.Keywords+strings.Join(dat.Categories, "")), "") if strings.Contains(matcher, query) { results = append(results, key) } } return results } var replaceNoteRe = regexp.MustCompile(`[\r\n]+`) func formatFeat(feat data, d Data, short bool) string { bold := color.New(color.Bold) var out strings.Builder out.WriteString(bold.Sprint(feat.Title)) var p []string if feat.UsagePercY > 0 { p = append(p, fmt.Sprintf("%s %s%%", resultmap["y"], color.GreenString("%.2f", feat.UsagePercY))) } if feat.UsagePercA > 0 { p = append(p, fmt.Sprintf("%s %s%%", resultmap["a"], color.YellowString("%.2f", feat.UsagePercA))) } percentages := strings.Join(p, " ") out.WriteString(" " + percentages) status := fmt.Sprintf(" [%s]\n", d.Statuses[feat.Status]) out.WriteString(status) if !short { out.WriteString("\t" + strings.TrimSpace(feat.Description) + "\n\n") } if short { out.WriteString("\t") } needNote := make(map[int]struct{}) browsers := make([]string, 0, len(feat.Stats)) for browser := range feat.Stats { if d.Agents[browser].Type != "desktop" { continue } browsers = append(browsers, browser) } sort.Strings(browsers) for _, browser := range browsers { agent := d.Agents[browser] stats := feat.Stats[browser] if !short { out.WriteString("\t") } out.WriteString(agent.Browser + " ") results := makeResults(agent, stats) if len(results) == 1 { results[0].version = "" } for _, result := range results { out.WriteString(makeResult(result, needNote)) } if !short { out.WriteString("\n") } } if !short { out.WriteString("\n") } if !short { nums := make([]string, 0, len(feat.NotesByNum)) for num := range feat.NotesByNum { nums = append(nums, num) } sort.Strings(nums) for _, num := range nums { n, _ := strconv.Atoi(num) if _, ok := needNote[n]; !ok { continue } out.WriteString(fmt.Sprintf("\t%s%s\n", color.YellowString(supernums[n]), feat.NotesByNum[num])) } if feat.Notes != "" { out.WriteString(fmt.Sprintf("\t%s %s", resultmap["i"], replaceNoteRe.ReplaceAllString(feat.Notes, " "))) } } return out.String() } var supportRe = regexp.MustCompile(`#(\d+)`) func makeResult(stat browserStat, nums map[int]struct{}) string { support := string(stat.support[0]) var out strings.Builder bace := "\u2800" if s, ok := resultmap[support]; ok { out.WriteString(s) } else { out.WriteString(support) } out.WriteString(bace) if stat.version != "" { out.WriteString(stat.version) } if strings.Contains(string(stat.support), "x") { out.WriteString("ᵖ") } match := supportRe.FindStringSubmatch(string(stat.support)) if match != nil { num, _ := strconv.Atoi(match[1]) nums[num] = struct{}{} out.WriteString(supernums[num]) } if stat.usage > 0 { str := out.String() if string(str[len(str)-1]) != bace { out.WriteString(" ") } out.WriteString(fmt.Sprintf("(%f) ", math.Round(stat.usage*1)/1)) } out.WriteString(" ") str := out.String() switch support { case "y": return color.GreenString(str) case "n": return color.RedString(str) case "a": return color.YellowString(str) default: return str } } func makeResults(browser browser, stats map[string]support) []browserStat { var results []browserStat var current browserStat for idx, version := range browser.Versions { if version == "" { continue } support := stats[version] var usage float64 if u, ok := browser.UsageGlobal[version]; ok { usage = u } ver := version if len(browser.Versions) > idx { ver += "+" } if support[0] == 'p' { support = "n" + support[1:] } if current.version == "" || current.support != support { current = browserStat{ version: ver, support: support, usage: 0, } results = append(results, current) } current.usage += usage } return results } func loadData() (Data, error) { var data Data cache, err := os.UserCacheDir() if err != nil { return data, fmt.Errorf("could not determine cache dir: %v", err) } caniuseCache := filepath.Join(cache, "caniuse") if err := os.MkdirAll(caniuseCache, os.ModePerm); err != nil { return data, fmt.Errorf("could not create dir %q: %v", caniuseCache, err) } dataPath := filepath.Join(caniuseCache, "data.json") fi, err := os.Stat(dataPath) if err != nil && !errors.Is(err, fs.ErrNotExist) { return data, fmt.Errorf("could not stat %q: %v", dataPath, err) } else if errors.Is(err, fs.ErrNotExist) || time.Since(fi.ModTime()) > refresh { color.Blue("updating caniuse data...") if err := Download(dataPath); err != nil { return data, fmt.Errorf("could not download data: %v", err) } } return Load(dataPath) }