summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--README.md9
-rw-r--r--build.go241
-rwxr-xr-xbuild.py168
-rwxr-xr-xfetch_and_build.sh2
-rw-r--r--go.mod10
-rw-r--r--go.sum17
-rw-r--r--head.html26
-rw-r--r--script.js18
-rw-r--r--style.css6
-rw-r--r--template.html.tmpl53
11 files changed, 346 insertions, 206 deletions
diff --git a/.gitignore b/.gitignore
index 23f3028..55b34c1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,4 @@
caniuse.rs/
rust/
-target/
+out/
lib_feats.txt
diff --git a/README.md b/README.md
index e69de29..64548ff 100644
--- a/README.md
+++ b/README.md
@@ -0,0 +1,9 @@
+# rust-features
+
+An autogenerated web page listing Rust features.
+
+Hosted under https://rust-features.push-f.com/.
+
+# Local development
+
+ go run build.go
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...)
+}
diff --git a/build.py b/build.py
deleted file mode 100755
index d0f7fba..0000000
--- a/build.py
+++ /dev/null
@@ -1,168 +0,0 @@
-#!/usr/bin/env python3
-import html
-import json
-import os
-import re
-import shutil
-import tomllib
-
-
-def get_features(dirname):
- feature_data = {}
- for feature in sorted(os.listdir('caniuse.rs/data/' + dirname)):
- with open('caniuse.rs/data/' + dirname + '/' + feature, 'rb') as f:
- name = feature.split('.')[0]
- feature_data[name] = tomllib.load(f)
-
- # new dict because we want to deduplicate features
- features = {}
-
- # caniuse.rs sometimes has several .toml files for one feature flag e.g.
- # For the const_io feature flag it has 6 .toml files, all with the same
- # tracking_issue_id but different titles.
- #
- # That makes sense for a search-centric application. Not so much for a
- # static-site generator since showing the same link 6 times is confusing.
- for feat, data in feature_data.items():
- key = data['flag'] if 'flag' in data else feat
-
- url = None
- if 'tracking_issue_id' in data:
- url = 'https://github.com/rust-lang/rust/issues/{}'.format(
- data['tracking_issue_id']
- )
- elif 'impl_pr_id' in data:
- url = 'https://github.com/rust-lang/rust/pull/{}'.format(data['impl_pr_id'])
- elif 'stabilization_pr_id' in data:
- url = 'https://github.com/rust-lang/rust/pull/{}'.format(
- data['stabilization_pr_id']
- )
- data['url'] = url
- data['filename'] = feat
-
- if data['title'].startswith('the '):
- data['title'] = data['title'][len('the ') :]
- data['title'] = data['title'].replace('implementation', 'impl')
-
- if key in features:
- data['title'] = data['flag'].replace('_', ' ')
-
- if key in features and features[key]['url'] != url:
- print(
- 'different urls for feature {}:\n* {}: {}\n* {}: {}'.format(
- key,
- data['filename'],
- data['url'],
- features[key]['filename'],
- features[key]['url'],
- )
- )
-
- features[key] = data
-
- features = dict(
- sorted(features.items(), key=lambda t: t[1]['title'].replace('`', '').lower())
- )
-
- lib_features = {}
- non_lib_features = {}
-
- for key, data in features.items():
- if ('flag' in data and data['flag'] in library_flags) or 'impl for' in data[
- 'title'
- ]:
- lib_features[key] = data
- else:
- non_lib_features[key] = data
-
- return dict(lib_features=lib_features, non_lib_features=non_lib_features)
-
-
-with open('lib_feats.txt') as f:
- library_flags = set([l.strip() for l in f])
-
-with open('caniuse.rs/data/versions.toml', 'rb') as f:
- versions = tomllib.load(f)
-
-for version, data in versions.items():
- try:
- data['features'] = get_features(version)
- except FileNotFoundError:
- pass
-
-versions = dict(reversed(list(versions.items())))
-
-unstable_features = get_features('unstable')
-
-
-os.makedirs('target', exist_ok=True)
-shutil.copy('style.css', 'target')
-shutil.copy('script.js', 'target')
-
-with open('target/data.json', 'w') as f:
- data = dict(unstable=dict(features=unstable_features), **versions)
- json.dump(data, f)
-
-
-def write_features(f, id, features):
- if features['non_lib_features']:
- write_feature_list(f, id + '-non-lib', features['non_lib_features'])
- if features['lib_features']:
- f.write('<h3 id="{}">library</h3>'.format(id + '-lib'))
- write_feature_list(f, id + '-lib', features['lib_features'])
-
-
-def write_feature_list(f, id, features):
- f.write('<ul id={}-list'.format(id))
- if len(features) > 5:
- f.write(' class=columns')
- f.write('>')
- for feat, data in features.items():
- f.write('<li><a')
- if 'flag' in data:
- f.write(' title="{}"'.format(data['flag']))
- url = data['url']
- if url:
- f.write(f' href="{url}"')
- f.write('>')
- title = html.escape(data['title'])
- title = re.sub('`(.+?)`', lambda m: '<code>{}</code>'.format(m.group(1)), title)
- f.write(title)
- f.write('</a></li>')
- f.write('</ul>')
-
-
-with open('target/index.html', 'w') as f:
- with open('head.html') as h:
- f.write(h.read())
-
- f.write('<h2 id=unstable>Unstable features</h2>')
- write_features(f, 'unstable', unstable_features)
-
- after_beta = False
-
- for version, data in versions.items():
- f.write('<div id={0} class=release>'.format(version))
- f.write('<h2>{}'.format(version))
- channel = data.get('channel')
- if after_beta:
- channel = 'stable'
- after_beta = False
- if channel:
- if channel == 'nightly':
- channel = 'stabilized'
- elif channel == 'beta':
- after_beta = True
- f.write(' [{}]'.format(channel))
- f.write('</h2>')
-
- if 'blog_post_path' in data:
- f.write(
- '<a class=release-notes href="https://blog.rust-lang.org/{}">{}</a>'.format(
- data['blog_post_path'], 'release notes'
- )
- )
- f.write('</div>')
-
- if 'features' in data:
- write_features(f, version, data['features'])
diff --git a/fetch_and_build.sh b/fetch_and_build.sh
index 760b03d..ba1c91c 100755
--- a/fetch_and_build.sh
+++ b/fetch_and_build.sh
@@ -2,4 +2,4 @@
(cd rust; git checkout master; git pull --ff-only)
./find.sh | grep -oP '(?<=feature = ")([^"]+)' > lib_feats.txt
rm -r build/
-./build.py
+go run build.go
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..37a793b
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,10 @@
+module push-f.com/rust-features
+
+go 1.23.5
+
+require (
+ github.com/BurntSushi/toml v1.5.0
+ github.com/sirupsen/logrus v1.9.3
+)
+
+require golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..4f5a5ea
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,17 @@
+github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
+github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
+github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
+golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/head.html b/head.html
deleted file mode 100644
index 2caa40b..0000000
--- a/head.html
+++ /dev/null
@@ -1,26 +0,0 @@
-<!doctype html>
-<html>
-<head>
-<meta charset=utf-8>
-<title>Rust features</title>
-<link rel="stylesheet" href="style.css" />
-<meta name="viewport" content="width=device-width, initial-scale=1">
-</head>
-<body>
-
-<div class=header>
-<div class=left><h1>Rust features</h1></div>
-<input id=search autocomplete=off tabindex=4>
-<noscript>
-With JavaScript you get a search box here.
-<style>
-#search { display: none; }
-</style>
-</noscript>
-<script src=script.js></script>
-<div class=right></div>
-</div>
-<div style="clear: both"></div>
-
-page generated by <a tabindex=1 target=_blank href="https://push-f.com/">push-f</a>
- from the <a tabindex=2 target=_blank href="https://github.com/jplatte/caniuse.rs">caniuse.rs dataset</a>
diff --git a/script.js b/script.js
index a631288..0e2491e 100644
--- a/script.js
+++ b/script.js
@@ -6,32 +6,32 @@
input.focus();
input.addEventListener('input', (e) => {
const query = e.target.value.toLowerCase();
- for (const [key, data] of Object.entries(versions)) {
- const heading = document.getElementById(key);
+ for (const version of versions) {
+ const heading = document.getElementById(version.name);
- if (data.features == undefined) {
+ if (version.lib.length == 0 && version.non_lib.length == 0) {
heading.hidden = query.length != 0;
continue;
}
- const nonLibResults = search(data.features.non_lib_features, query);
- const libResults = search(data.features.lib_features, query);
+ const nonLibResults = search(version.non_lib, query);
+ const libResults = search(version.lib, query);
const totalResultCount = nonLibResults.length + libResults.length;
// so that release notes don't get in the way when <Tab>ing through results
document.body.classList.toggle('hide-release-notes', totalResultCount == 0);
- let list = document.getElementById(key + '-non-lib-list');
+ let list = document.getElementById(version.name + '-non-lib-list');
if (list) {
list.replaceChildren(...renderList(nonLibResults).children);
list.hidden = nonLibResults.length == 0;
}
- list = document.getElementById(key + '-lib-list');
+ list = document.getElementById(version.name + '-lib-list');
if (list) {
list.replaceChildren(...renderList(libResults).children);
list.hidden = libResults.length == 0;
- document.getElementById(key + '-lib').hidden = libResults.length == 0;
+ document.getElementById(version.name + '-lib-heading').hidden = libResults.length == 0;
}
heading.hidden = totalResultCount == 0;
@@ -40,7 +40,7 @@
})();
function search(features, query) {
- return Object.values(features).filter((feat) => {
+ return features.filter((feat) => {
if (feat.title.toLowerCase().replaceAll('`', '').includes(query)) {
return true;
}
diff --git a/style.css b/style.css
index 8c8559f..6ad3fd2 100644
--- a/style.css
+++ b/style.css
@@ -2,7 +2,11 @@ body {
font-family: sans;
}
-.columns {
+a {
+ text-decoration: none;
+}
+
+ul {
columns: 300px;
}
diff --git a/template.html.tmpl b/template.html.tmpl
new file mode 100644
index 0000000..d993725
--- /dev/null
+++ b/template.html.tmpl
@@ -0,0 +1,53 @@
+<!doctype html>
+<html>
+<head>
+<meta charset=utf-8>
+<title>Rust features</title>
+<link rel="stylesheet" href="style.css" />
+<meta name="viewport" content="width=device-width, initial-scale=1">
+</head>
+<body>
+
+<div class=header>
+<div class=left><h1>Rust features</h1></div>
+<input id=search autocomplete=off tabindex=4>
+<noscript>
+With JavaScript you get a search box here.
+<style>
+#search { display: none; }
+</style>
+</noscript>
+<script src=script.js></script>
+<div class=right></div>
+</div>
+<div style="clear: both"></div>
+
+page generated by <a tabindex=1 target=_blank href="https://push-f.com/">push-f</a>
+ from the <a tabindex=2 target=_blank href="https://github.com/jplatte/caniuse.rs">caniuse.rs dataset</a>
+
+{{range .Versions}}
+ <div class=release>
+ <h2 id="{{.Name}}">{{.Name}}</h2>
+ {{with .BlogPostURL}}
+ <a href="{{.}}" class=release-notes>release notes</a>
+ {{end}}
+ </div>
+ {{if .NonLibFeatures}}
+ <ul id="{{.Name}}-non-lib-list">
+ {{- range .NonLibFeatures}}
+ <li>{{template "feature" .}}</li>
+ {{- end}}
+ </ul>
+ {{end}}
+ {{if .LibFeatures}}
+ <h3 id="{{.Name}}-lib-heading">library</h3>
+ <ul id="{{.Name}}-lib-list">
+ {{- range .LibFeatures}}
+ <li>{{template "feature" .}}</li>
+ {{- end}}
+ </ul>
+ {{end}}
+{{end}}
+{{define "feature" -}}
+ <a {{with .URL}}href="{{.}}"{{end}} {{with .Flag}}title="{{.}}"{{end}}>{{.Title | formatBackticks}}</a>
+{{- end}}