286 lines
5.9 KiB
Go
286 lines
5.9 KiB
Go
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 (
|
|
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)
|
|
|
|
if err := ff.Parse(fs, os.Args[1:],
|
|
ff.WithEnvVarPrefix("CANIUSE"),
|
|
); err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
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{})
|
|
for browser, stats := range feat.Stats {
|
|
if d.Agents[browser].Type != "desktop" {
|
|
continue
|
|
}
|
|
if !short {
|
|
out.WriteString("\t")
|
|
}
|
|
out.WriteString(d.Agents[browser].Browser + " ")
|
|
|
|
results := makeResults(d.Agents[browser], 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\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 {
|
|
if err := Download(dataPath); err != nil {
|
|
return data, fmt.Errorf("could not download data: %v", err)
|
|
}
|
|
}
|
|
|
|
return Load(dataPath)
|
|
}
|