1
0
mirror of https://github.com/s00500/ESPUI.git synced 2026-02-05 17:02:41 +00:00
Files
ESPUI/tools/prepare_static_ui_sources.go
2026-01-28 09:22:04 +01:00

360 lines
9.2 KiB
Go

// Package main provides a tool to prepare ESPUI header files by minifying
// and gzipping HTML, JS, and CSS source files.
//
// Usage:
//
// go run prepare_static_ui_sources.go -a
// go run prepare_static_ui_sources.go -s ../data -t ../src
//
// Or build and run:
//
// go build -o prepare_static_ui_sources prepare_static_ui_sources.go
// ./prepare_static_ui_sources -a
package main
import (
"bytes"
"compress/gzip"
"flag"
"fmt"
"io/fs"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/tdewolff/minify/v2"
"github.com/tdewolff/minify/v2/css"
"github.com/tdewolff/minify/v2/html"
"github.com/tdewolff/minify/v2/js"
)
const targetTemplate = `const char %s[] PROGMEM = R"=====(
%s
)=====";
const uint8_t %s_GZIP[%d] PROGMEM = { %s };
`
type context struct {
infile string
outfile string
outdir string
outfilename string
minifile string
constant string
fileType string
name string
dir string
minidata string
gzipdata string
gziplen int
}
func main() {
auto := flag.Bool("a", false, "Automatically find all source files in data/ and write C header files to src/")
flag.BoolVar(auto, "auto", false, "Automatically find all source files in data/ and write C header files to src/")
flag.BoolVar(auto, "all", false, "Automatically find all source files in data/ and write C header files to src/")
sources := flag.String("s", "", "Sources directory containing CSS or JS files OR one specific file to minify")
flag.StringVar(sources, "source", "", "Sources directory containing CSS or JS files OR one specific file to minify")
flag.StringVar(sources, "sources", "", "Sources directory containing CSS or JS files OR one specific file to minify")
target := flag.String("t", "", "Target directory containing C header files OR one C header file")
flag.StringVar(target, "target", "", "Target directory containing C header files OR one C header file")
storeMini := flag.Bool("storemini", true, "Store intermediate minified files next to the originals")
noStoreMini := flag.Bool("nostoremini", false, "Do not store intermediate minified files")
flag.BoolVar(noStoreMini, "m", false, "Do not store intermediate minified files")
flag.Parse()
// Handle nostoremini flag
if *noStoreMini {
*storeMini = false
}
if !*auto && (*sources == "" || *target == "") {
fmt.Println("ERROR: You need to specify either --auto/-a or both --source/-s and --target/-t")
fmt.Println()
flag.Usage()
os.Exit(1)
}
// Get the directory where this script is located
execPath, err := os.Executable()
if err != nil {
// Fallback to current working directory
execPath, _ = os.Getwd()
execPath = filepath.Join(execPath, "tools", "dummy")
}
scriptDir := filepath.Dir(execPath)
// For "go run", use the current working directory approach
if strings.Contains(execPath, "go-build") {
cwd, _ := os.Getwd()
scriptDir = cwd
}
// Set default paths if auto mode
if *sources == "" {
*sources = filepath.Join(scriptDir, "..", "data")
}
if *target == "" {
*target = filepath.Join(scriptDir, "..", "src")
}
// Resolve to absolute paths
*sources, _ = filepath.Abs(*sources)
*target, _ = filepath.Abs(*target)
if err := checkArgs(*sources, *target); err != nil {
fmt.Printf("ERROR: %v\n", err)
fmt.Println("Aborting.")
os.Exit(1)
}
info, err := os.Stat(*sources)
if err != nil {
fmt.Printf("ERROR: Cannot stat source: %v\n", err)
os.Exit(1)
}
if info.IsDir() {
fmt.Printf("Source %s is a directory, searching for files recursively...\n", *sources)
if err := processDir(*sources, *target, *storeMini); err != nil {
fmt.Printf("ERROR: %v\n", err)
os.Exit(1)
}
} else {
fmt.Printf("Source %s is a file, will process one file only.\n", *sources)
if err := processFile(*sources, *target, *storeMini); err != nil {
fmt.Printf("ERROR: %v\n", err)
os.Exit(1)
}
}
}
func checkArgs(sources, target string) error {
if _, err := os.Stat(sources); os.IsNotExist(err) {
return fmt.Errorf("source %s does not exist", sources)
}
targetParent := filepath.Dir(target)
if info, err := os.Stat(targetParent); err != nil || !info.IsDir() {
return fmt.Errorf("parent directory of target %s does not exist", target)
}
sourceInfo, _ := os.Stat(sources)
targetInfo, targetErr := os.Stat(target)
if sourceInfo.IsDir() && (targetErr != nil || !targetInfo.IsDir()) {
return fmt.Errorf("source %s is a directory, target %s is not", sources, target)
}
return nil
}
func getContext(infile, outfile string) (*context, error) {
infile, err := filepath.Abs(infile)
if err != nil {
return nil, err
}
// Extract directory, name, and type
dir := filepath.Base(filepath.Dir(infile))
base := filepath.Base(infile)
ext := filepath.Ext(base)
name := strings.TrimSuffix(base, ext)
// Remove any .min suffix from name for constant generation
name = strings.TrimSuffix(name, ".min")
fileType := strings.TrimPrefix(strings.ToLower(ext), ".")
// If directory name matches the file type, go up one level
if strings.ToLower(dir) == fileType {
dir = filepath.Base(filepath.Dir(filepath.Dir(infile)))
}
// Normalize htm to html
if fileType == "htm" {
fileType = "html"
}
indir := filepath.Dir(infile)
c := &context{
infile: infile,
fileType: fileType,
name: name,
dir: dir,
}
// Determine output file
info, err := os.Stat(outfile)
if err == nil && info.IsDir() {
c.outdir, _ = filepath.Abs(outfile)
c.outfilename = fmt.Sprintf("%s%s%s.h", dir, capitalize(name), strings.ToUpper(fileType))
c.outfile = filepath.Join(c.outdir, c.outfilename)
} else {
c.outfile, _ = filepath.Abs(outfile)
c.outdir = filepath.Dir(c.outfile)
c.outfilename = filepath.Base(c.outfile)
}
// Determine minified file path
if strings.Contains(infile, ".min.") {
c.minifile = infile
} else {
// Replace .ext with .min.ext
c.minifile = filepath.Join(indir, strings.TrimSuffix(base, ext)+".min"+ext)
}
// Generate constant name
c.constant = fmt.Sprintf("%s_%s", strings.ToUpper(fileType), strings.ToUpper(name))
return c, nil
}
func capitalize(s string) string {
if len(s) == 0 {
return s
}
return strings.ToUpper(s[:1]) + s[1:]
}
func performGzip(c *context) error {
var buf bytes.Buffer
gz := gzip.NewWriter(&buf)
if _, err := gz.Write([]byte(c.minidata)); err != nil {
return err
}
if err := gz.Close(); err != nil {
return err
}
compressed := buf.Bytes()
c.gziplen = len(compressed)
// Convert bytes to comma-separated string
parts := make([]string, len(compressed))
for i, b := range compressed {
parts[i] = fmt.Sprintf("%d", b)
}
c.gzipdata = strings.Join(parts, ",")
fmt.Printf(" GZIP data length: %d\n", c.gziplen)
return nil
}
func performMinify(c *context) error {
data, err := os.ReadFile(c.infile)
if err != nil {
return err
}
m := minify.New()
m.AddFunc("text/css", css.Minify)
m.AddFunc("text/html", html.Minify)
m.AddFunc("application/javascript", js.Minify)
m.AddFunc("text/javascript", js.Minify)
var mediaType string
switch c.fileType {
case "css":
mediaType = "text/css"
case "js":
mediaType = "application/javascript"
case "html", "htm":
mediaType = "text/html"
default:
mediaType = "text/html"
}
fmt.Printf(" Using %s minifier\n", c.fileType)
minified, err := m.String(mediaType, string(data))
if err != nil {
return fmt.Errorf("minification failed: %w", err)
}
c.minidata = minified
return performGzip(c)
}
func processFile(infile, outdir string, storeMini bool) error {
fmt.Printf("Processing file %s\n", infile)
c, err := getContext(infile, outdir)
if err != nil {
return err
}
if err := performMinify(c); err != nil {
return err
}
if storeMini {
if c.infile == c.minifile {
fmt.Println(" Original file is already minified, refusing to overwrite it")
} else {
fmt.Printf(" Writing minified file %s\n", c.minifile)
if err := os.WriteFile(c.minifile, []byte(c.minidata), 0644); err != nil {
return err
}
}
}
fmt.Printf(" Using C constant names %s and %s_GZIP\n", c.constant, c.constant)
fmt.Printf(" Writing C header file %s\n", c.outfile)
output := fmt.Sprintf(targetTemplate, c.constant, c.minidata, c.constant, c.gziplen, c.gzipdata)
return os.WriteFile(c.outfile, []byte(output), 0644)
}
func processDir(sourceDir, outDir string, storeMini bool) error {
pattern := regexp.MustCompile(`(?i)\.(css|js|htm|html)$`)
processed := make(map[string]bool)
err := filepath.WalkDir(sourceDir, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
return nil
}
if !pattern.MatchString(path) {
return nil
}
// Skip if already processed
if processed[path] {
return nil
}
// Check if this is a .min. file
if strings.Contains(filepath.Base(path), ".min.") {
// Only process .min. files if the non-minified version doesn't exist
nonMinPath := strings.Replace(path, ".min.", ".", 1)
if _, err := os.Stat(nonMinPath); err == nil {
// Non-minified version exists, skip this .min. file
return nil
}
} else {
// Mark corresponding .min. file as processed to avoid duplicate processing
ext := filepath.Ext(path)
minPath := strings.TrimSuffix(path, ext) + ".min" + ext
processed[minPath] = true
}
processed[path] = true
return processFile(path, outDir, storeMini)
})
return err
}