package main import ( "bytes" _ "embed" "encoding/json" "flag" "fmt" "html/template" "log/slog" "maps" "os" "os/exec" "regexp" "slices" "strconv" "strings" "github.com/BurntSushi/toml" ) const caniuseRepoURL = "https://github.com/jplatte/caniuse.rs" const rustRepoURL = "https://github.com/rust-lang/rust" var codeRegex = regexp.MustCompile("`(.+?)`") //go:embed template.html.tmpl var templateText string //go:embed script.js var script []byte //go:embed style.css var style []byte //go:embed find-lib-feats.sh var findLibFeatsShellCommand string var log *slog.Logger func main() { log = slog.New(slog.NewTextHandler(os.Stderr, nil)) caniuseRepo := "caniuse.rs" rustRepo := "rust" outDir := "out" skipDownload := flag.Bool("skip-download", false, fmt.Sprintf("skip cloning/updating the %s and %s repos", caniuseRepo, rustRepo)) flag.Parse() if !*skipDownload { if _, err := os.Stat(caniuseRepo); os.IsNotExist(err) { runCommand("git", "clone", caniuseRepoURL, caniuseRepo) } else { runCommand("git", "-C", caniuseRepo, "pull") } if _, err := os.Stat(rustRepo); os.IsNotExist(err) { runCommand("git", "clone", rustRepoURL, rustRepo, "--depth", "1") } else { runCommand("git", "-C", rustRepo, "fetch", "--depth", "1") runCommand("git", "-C", rustRepo, "reset", "--hard", "origin/master") } } cmd := exec.Command("sh", "-c", findLibFeatsShellCommand, "find-lib-feats.sh", rustRepo) var stderr bytes.Buffer cmd.Stderr = &stderr libFeaturesText, err := cmd.Output() if err != nil { log.Error("failed to run shell command to find library features", Error(err), "stderr", stderr.String()) os.Exit(1) } libFeatureFlags := strings.Split(string(libFeaturesText), "\n") versionInfos := make(map[string]VersionInfo) versionsPath := fmt.Sprintf(caniuseRepo + "/data/versions.toml") _, err = toml.DecodeFile(versionsPath, &versionInfos) if err != nil { log.Error("failed to parse versions.toml", Error(err), "path", versionsPath) os.Exit(1) } if len(versionInfos) == 0 { log.Error("found no versions in versions.toml") os.Exit(1) } versions := make([]Version, 0) sortedVersions := slices.SortedFunc(maps.Keys(versionInfos), compareVersion) slices.Reverse(sortedVersions) featureCount := 0 for _, name := range append([]string{"unstable"}, sortedVersions...) { features := getFeatures(caniuseRepo, name) libFeatures := make([]Feature, 0) nonLibFeatures := make([]Feature, 0) for _, feature := range features { if slices.Contains(libFeatureFlags, feature.Flag) || strings.Contains(feature.Title, " impl for ") { libFeatures = append(libFeatures, feature) } else { nonLibFeatures = append(nonLibFeatures, feature) } } version := Version{ Name: name, LibFeatures: nilAsEmptyArray(slices.SortedFunc(slices.Values(libFeatures), compareFeature)), NonLibFeatures: nilAsEmptyArray(slices.SortedFunc(slices.Values(nonLibFeatures), compareFeature)), } versionInfo := versionInfos[name] if versionInfo.BlogPostPath != nil { url := fmt.Sprintf("https://blog.rust-lang.org/%s", *versionInfo.BlogPostPath) version.BlogPostURL = &url } versions = append(versions, version) featureCount += len(features) } if featureCount == 0 { log.Error("found no features") os.Exit(1) } tmpl := template.New("template.html.tmpl").Funcs(template.FuncMap{ "formatBackticks": func(s string) template.HTML { return template.HTML(codeRegex.ReplaceAllStringFunc(s, func(code string) string { return fmt.Sprintf( "%s", template.HTMLEscapeString(codeRegex.FindStringSubmatch(code)[1]), ) })) }, }) tmpl, err = tmpl.Parse(templateText) if err != nil { log.Error("error parsing template", Error(err)) os.Exit(1) } err = os.MkdirAll(outDir, 0o755) if err != nil { log.Error("error creating output directory", Error(err)) os.Exit(1) } outputFile, err := os.Create(outDir + "/index.html") if err != nil { log.Error("error creating index.html", Error(err)) os.Exit(1) } defer outputFile.Close() err = tmpl.Execute(outputFile, map[string]any{ "Versions": versions, "CaniuseRepoURL": caniuseRepoURL, }, ) if err != nil { log.Error("error executing template", Error(err)) os.Exit(1) } outputFile, err = os.Create(outDir + "/data.json") if err != nil { log.Error("error creating data.json", Error(err)) os.Exit(1) } defer outputFile.Close() encoder := json.NewEncoder(outputFile) err = encoder.Encode(versions) if err != nil { log.Error("error encoding JSON", Error(err)) os.Exit(1) } err = os.WriteFile(outDir+"/script.js", script, 0o644) if err != nil { log.Error("error creating script.js", Error(err)) os.Exit(1) } err = os.WriteFile(outDir+"/style.css", style, 0o644) if err != nil { log.Error("error creating style.css", Error(err)) os.Exit(1) } } type Version struct { Name string `json:"name"` BlogPostURL *string `json:"-"` LibFeatures []Feature `json:"lib"` NonLibFeatures []Feature `json:"non_lib"` } // Version info from "caniuse.rs/data/versions.toml". type VersionInfo struct { BlogPostPath *string `toml:"blog_post_path"` } // Data from a .toml file in "caniuse.rs/data/{version}/". type Feature struct { Title string `toml:"title" json:"title"` Flag string `toml:"flag" json:"flag"` TrackingIssueId *uint `toml:"tracking_issue_id" json:"-"` ImplPrId *uint `toml:"impl_pr_id" json:"-"` StabilizationPrId *uint `toml:"stabilization_pr_id" json:"-"` Aliases []string `toml:"aliases" json:"aliases"` Items []string `toml:"items" json:"items"` URL string `json:"url"` } func getFeatures(caniuseRepo string, version string) map[string]Feature { dirPath := caniuseRepo + "/data/" + version entries, err := os.ReadDir(dirPath) if err != nil { if os.IsNotExist(err) { return nil } log.Error("failed to read directory", "path", dirPath, Error(err)) os.Exit(1) } features := make(map[string]Feature) for _, entry := range entries { var feature Feature path := dirPath + "/" + entry.Name() _, err := toml.DecodeFile(path, &feature) if err != nil { log.Error("error decoding TOML file", "path", path, Error(err)) os.Exit(1) } if feature.TrackingIssueId != nil { feature.URL = fmt.Sprintf("https://github.com/rust-lang/rust/issues/%d", *feature.TrackingIssueId) } else if feature.ImplPrId != nil { feature.URL = fmt.Sprintf("https://github.com/rust-lang/rust/pull/%d", *feature.ImplPrId) } else if feature.StabilizationPrId != nil { feature.URL = fmt.Sprintf("https://github.com/rust-lang/rust/pull/%d", *feature.StabilizationPrId) } feature.Title = strings.TrimPrefix(feature.Title, "the ") feature.Title = strings.Replace(feature.Title, "implementation", "impl", 1) var key string if feature.Flag == "" { key = entry.Name() } else { key = feature.Flag } _, exists := features[key] if exists { // caniuse.rs sometimes intentionally has several .toml files // for the same feature flag (with different titles). // In this case we only want to display the feature once. feature.Title = feature.Flag } features[key] = feature } return features } func compareFeature(a, b Feature) int { aTitle := strings.ToLower(strings.TrimLeft(a.Title, "`")) bTitle := strings.ToLower(strings.TrimLeft(b.Title, "`")) if aTitle > bTitle { return 1 } else if aTitle < bTitle { return -1 } return 0 } func compareVersion(a, b string) int { aParts := strings.Split(a, ".") bParts := strings.Split(b, ".") aMajor, _ := strconv.Atoi(aParts[0]) bMajor, _ := strconv.Atoi(bParts[0]) if aMajor != bMajor { return aMajor - bMajor } aMinor, _ := strconv.Atoi(aParts[1]) bMinor, _ := strconv.Atoi(bParts[1]) return aMinor - bMinor } func runCommand(name string, args ...string) { cmd := exec.Command(name, args...) var stderr bytes.Buffer cmd.Stderr = &stderr if err := cmd.Run(); err != nil { log.Error("failed to run command", "command", cmd.String(), Error(err), "stderr", stderr.String()) os.Exit(1) } } // workaround for https://github.com/golang/go/issues/37711 func nilAsEmptyArray[T any](slice []T) []T { return append([]T{}, slice...) } func Error(value error) slog.Attr { return slog.String("error", value.Error()) }