mirror of
https://github.com/s00500/ESPUI.git
synced 2026-02-05 17:02:41 +00:00
Add go version of helper tool
Signed-off-by: Lukas Bachschwell <lukas@lbsfilm.at>
This commit is contained in:
7
tools/go.mod
Normal file
7
tools/go.mod
Normal file
@@ -0,0 +1,7 @@
|
||||
module github.com/me-no-dev/ESPUItools
|
||||
|
||||
go 1.21
|
||||
|
||||
require github.com/tdewolff/minify/v2 v2.21.2
|
||||
|
||||
require github.com/tdewolff/parse/v2 v2.7.19 // indirect
|
||||
7
tools/go.sum
Normal file
7
tools/go.sum
Normal file
@@ -0,0 +1,7 @@
|
||||
github.com/tdewolff/minify/v2 v2.21.2 h1:VfTvmGVtBYhMTlUAeHtXM7XOsW0JT/6uMwUPPqgUs9k=
|
||||
github.com/tdewolff/minify/v2 v2.21.2/go.mod h1:Olje3eHdBnrMjINKffDsil/3NV98Iv7MhWf7556WQVg=
|
||||
github.com/tdewolff/parse/v2 v2.7.19 h1:7Ljh26yj+gdLFEq/7q9LT4SYyKtwQX4ocNrj45UCePg=
|
||||
github.com/tdewolff/parse/v2 v2.7.19/go.mod h1:3FbJWZp3XT9OWVN3Hmfp0p/a08v4h8J9W1aghka0soA=
|
||||
github.com/tdewolff/test v1.0.11-0.20231101010635-f1265d231d52/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE=
|
||||
github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739 h1:IkjBCtQOOjIn03u/dMQK9g+Iw9ewps4mCl1nB8Sscbo=
|
||||
github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
|
||||
359
tools/prepare_static_ui_sources.go
Normal file
359
tools/prepare_static_ui_sources.go
Normal file
@@ -0,0 +1,359 @@
|
||||
// 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
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# script is kept for legacy reasons, please use go version instead!
|
||||
|
||||
from jsmin import jsmin as jsminify
|
||||
try:
|
||||
from htmlmin import minify as htmlminify
|
||||
|
||||
Reference in New Issue
Block a user