diff options
Diffstat (limited to 'build.go')
-rw-r--r-- | build.go | 241 |
1 files changed, 241 insertions, 0 deletions
diff --git a/build.go b/build.go new file mode 100644 index 0000000..9e91800 --- /dev/null +++ b/build.go @@ -0,0 +1,241 @@ +package main + +import ( + _ "embed" + "encoding/json" + "fmt" + "html/template" + "maps" + "os" + "regexp" + "slices" + "strconv" + "strings" + + "github.com/BurntSushi/toml" + log "github.com/sirupsen/logrus" +) + +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 + +func main() { + caniuseRepo := "caniuse.rs" + outDir := "out" + + libFeaturesText, err := os.ReadFile("lib_feats.txt") + if err != nil { + log.Fatalf("error reading lib_feats.txt: %s", err) + } + 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.Fatalf("error parsing %s: %s", versionsPath, err) + } + + if len(versionInfos) == 0 { + log.Fatal("found no versions in versions.toml") + } + + versions := make([]Version, 0) + sortedVersions := slices.SortedFunc(maps.Keys(versionInfos), compareVersion) + slices.Reverse(sortedVersions) + + for _, name := range append([]string{"unstable"}, sortedVersions...) { + features := getFeatures(caniuseRepo, name) + if len(features) == 0 { + log.Infof("no features found for %s", 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) + } + + 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( + "<code>%s</code>", + template.HTMLEscapeString(codeRegex.FindStringSubmatch(code)[1]), + ) + })) + }, + }) + tmpl, err = tmpl.Parse(templateText) + + if err != nil { + log.Fatalf("error parsing template: %s", err) + } + + err = os.MkdirAll(outDir, 0o755) + if err != nil { + log.Fatalf("error creating directory: %s", err) + } + + outputFile, err := os.Create(outDir + "/index.html") + if err != nil { + log.Fatalf("error creating index.html: %s", err) + } + defer outputFile.Close() + + err = tmpl.Execute(outputFile, + map[string]any{ + "Versions": versions, + }, + ) + if err != nil { + log.Fatalf("error executing template: %s", err) + } + + outputFile, err = os.Create(outDir + "/data.json") + if err != nil { + log.Fatalf("error creating data.json: %s", err) + } + defer outputFile.Close() + + encoder := json.NewEncoder(outputFile) + err = encoder.Encode(versions) + if err != nil { + log.Fatalf("error encoding JSON: %s", err) + } + + err = os.WriteFile(outDir+"/script.js", script, 0o644) + if err != nil { + log.Fatalf("error creating script.js: %s", err) + } + + err = os.WriteFile(outDir+"/style.css", style, 0o644) + if err != nil { + log.Fatalf("error creating style.css: %s", err) + } +} + +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.Fatal(err) + } + + features := make(map[string]Feature) + + for _, entry := range entries { + var feature Feature + _, err := toml.DecodeFile(dirPath+"/"+entry.Name(), &feature) + if err != nil { + log.Fatalf("Error parsing %s: %s\n", dirPath+entry.Name(), err) + } + 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 +} + +// workaround for https://github.com/golang/go/issues/37711 +func nilAsEmptyArray[T any](slice []T) []T { + return append([]T{}, slice...) +} |