summaryrefslogtreecommitdiff
path: root/build.go
diff options
context:
space:
mode:
Diffstat (limited to 'build.go')
-rw-r--r--build.go241
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...)
+}