blob: bfe491a3439c8487bb3d1616446ddbfabf5d7bf1 [file] [log] [blame]
// This program generates custom elements for various SVG icons found in the material-design-icons
// NPM package (https://github.com/google/material-design-icons). All generated custom elements
// will be placed in directories following the //elements-sk/modules/icons/<icon-name>-sk naming
// scheme.
//
// This program also generates file //elements-sk/modules/icon-demo-sk/icons.ts, which is used by
// the icons-demo-sk custom element to showcase all icons.
//
// Usage:
//
// $ bazelisk run --config=mayberemote //elements-sk/generate_icons
package main
import (
"fmt"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
"go.skia.org/infra/elements-sk/generate_icons/demo"
"go.skia.org/infra/elements-sk/generate_icons/icon"
"go.skia.org/infra/go/httputils"
"go.skia.org/infra/go/skerr"
"go.skia.org/infra/go/sklog"
"go.skia.org/infra/go/untar"
)
var iconCategories = []string{
"action",
"alert",
"av",
"communication",
"content",
"device",
"editor",
"file",
"hardware",
"image",
"maps",
"navigation",
"notification",
"places",
"social",
"toggle",
}
// We download the material-design-icons NPM package manually, rather than listing it in
// //package.json, because we don't expect to run this program often, and said package is not used
// anywhere else in our codebase.
const materialDesignIconsNPMPackageURL = "https://registry.npmjs.org/material-design-icons/-/material-design-icons-3.0.1.tgz"
// There is one directory per icon category within the NPM package.
const iconCategoryDirTmpl = "package/%s/svg/production/"
// Each icon has multiple associated files (e.g. different sizes), but we only care about one.
var iconFileNameRegexp = regexp.MustCompile(`(ic_)?(?P<name>.+)_24px.svg`)
func main() {
// Get the path to the repository root (and ensure we are running under Bazel).
workspaceDir := os.Getenv("BUILD_WORKSPACE_DIRECTORY")
if workspaceDir == "" {
sklog.Fatal("The BUILD_WORKSPACE_DIRECTORY environment variable is not set. Are you running this program via Bazel?")
}
// Download material-design-icons NPM package.
materialDesignIconsDir, cleanupFn, err := downloadAndExtractMaterialDesignIconsNPMPackage()
if err != nil {
sklog.Fatalf("Error downloading/extracting the material-design-icons NPM package: %s", err)
}
defer cleanupFn()
// The icons-demo-sk custom element will display icons grouped by category.
iconNamesByCategory := map[string][]string{}
// For simplicity, all our icon elements are generated within the same directory. However, some
// icon names appear in multiple categories, causing a file name clash in the directory where
// icon elements are generated. for example:
//
// - https://github.com/google/material-design-icons/blob/3.0.1/places/svg/production/ic_rv_hookup_24px.svg
// - https://github.com/google/material-design-icons/blob/3.0.1/notification/svg/production/ic_rv_hookup_24px.svg
//
// To remedy this, we only generate an icon element for the first ocurrence of a given icon
// name.
allIconNames := map[string]bool{}
// Ensure that we'll scan icon category directories alphabetically.
sort.Strings(iconCategories)
for _, iconCategory := range iconCategories {
iconNamesByCategory[iconCategory] = []string{}
// Scan directory for the current icon category.
iconsDirPath := filepath.Join(materialDesignIconsDir, fmt.Sprintf(iconCategoryDirTmpl, iconCategory))
sklog.Infof("Scanning directory: %s", iconsDirPath)
files, err := os.ReadDir(iconsDirPath)
if err != nil {
sklog.Fatalf("Error reading from directory %s: %s", iconsDirPath, err)
}
// Iterate over all files in the current icon category directory.
for _, file := range files {
if match := iconFileNameRegexp.FindStringSubmatch(file.Name()); match != nil {
iconName := sanitizeIconName(match[2])
// Ignore file if we have already observed an icon with the same name.
if allIconNames[iconName] {
continue
}
allIconNames[iconName] = true
iconNamesByCategory[iconCategory] = append(iconNamesByCategory[iconCategory], iconName)
// Generate a custom element for the current icon.
iconSvgPath := filepath.Join(iconsDirPath, file.Name())
err := icon.Generate(workspaceDir, iconName, iconSvgPath)
if err != nil {
sklog.Fatalf("Error generating custom element for icon file %s: %s", iconSvgPath, err)
}
sklog.Infof("Generated custom element for icon: %s", iconName)
}
}
// Sort icons by name, as sanitization might have broken the lexicographic order
// (e.g. "3d" -> "three-d").
sort.Strings(iconNamesByCategory[iconCategory])
}
// Generate the icons.ts file used by the icons-demo-sk custom element.
if err := demo.Generate(workspaceDir, iconNamesByCategory); err != nil {
sklog.Fatalf("Error icons.ts: %s", err)
}
}
// downloadAndExtractMaterialDesignIconsNPMPackage downloads the material-design-icons NPM package
// and extracts the archive in a temporary directory. It returns the path to the directory with the
// extracted archive and a cleanup function to delete said directory.
func downloadAndExtractMaterialDesignIconsNPMPackage() (string, func(), error) {
// Create a temporary directory where we will extract the downloaded archive.
materialDesignIconsDir, err := os.MkdirTemp("/tmp", "material-design-icons-*")
if err != nil {
sklog.Fatalf("Error creating temporary directory: %s", err)
}
cleanupFn := func() {
if err := os.RemoveAll(materialDesignIconsDir); err != nil {
sklog.Infof("Error deleting temporary directory: %s", err)
}
}
// Download NPM package.
sklog.Infof("Downloading material-design-icons NPM package from: %s", materialDesignIconsNPMPackageURL)
resp, err := httputils.NewTimeoutClient().Get(materialDesignIconsNPMPackageURL)
if err != nil {
cleanupFn()
return "", func() {}, skerr.Wrap(err)
}
defer resp.Body.Close()
// Extract NPM package.
if err := untar.Untar(resp.Body, materialDesignIconsDir); err != nil {
cleanupFn()
return "", func() {}, skerr.Wrap(err)
}
return materialDesignIconsDir, cleanupFn, nil
}
func sanitizeIconName(s string) string {
s = strings.Replace(s, "_", "-", -1)
if strings.HasPrefix(s, "3d") {
s = strings.Replace(s, "3d", "three-d", -1)
}
return s
}