diff options
| -rw-r--r-- | .gitignore | 2 | ||||
| -rw-r--r-- | README.md | 9 | ||||
| -rw-r--r-- | build.go | 241 | ||||
| -rwxr-xr-x | build.py | 168 | ||||
| -rwxr-xr-x | fetch_and_build.sh | 2 | ||||
| -rw-r--r-- | go.mod | 10 | ||||
| -rw-r--r-- | go.sum | 17 | ||||
| -rw-r--r-- | head.html | 26 | ||||
| -rw-r--r-- | script.js | 18 | ||||
| -rw-r--r-- | style.css | 6 | ||||
| -rw-r--r-- | template.html.tmpl | 53 | 
11 files changed, 346 insertions, 206 deletions
@@ -1,4 +1,4 @@  caniuse.rs/  rust/ -target/ +out/  lib_feats.txt @@ -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 @@ -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 @@ -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> @@ -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;  		} @@ -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}}  | 
