themes/main.go

242 lines
5.6 KiB
Go

package main
import (
_ "embed"
"encoding/json"
"flag"
"html/template"
"net/http"
"net/url"
"os"
"os/exec"
"path/filepath"
"sort"
"strings"
"sync"
"time"
"github.com/charmbracelet/log"
"github.com/radovskyb/watcher"
)
var (
Logger = log.NewWithOptions(os.Stderr, log.Options{
ReportTimestamp: true,
ReportCaller: true,
})
Projects = []string{
"lavender",
}
DefaultFuncMap = template.FuncMap{
"emailHide": EmailHide,
}
ProjectPaths = make(map[string]bool)
TemplateMu = &sync.RWMutex{}
TemplateMap = make(map[string]*template.Template)
TemplateData = make(map[string]any)
//go:embed main-index.go.html
MainIndexRaw string
MainIndexTemplate = template.Must(template.New("main-index").Parse(MainIndexRaw))
WatchMode bool
DebugMode bool
)
func main() {
flag.BoolVar(&WatchMode, "watch", false, "watch mode")
flag.BoolVar(&DebugMode, "debug", false, "debug mode")
flag.Parse()
if DebugMode {
Logger.SetLevel(log.DebugLevel)
}
Logger.Info("Starting theme development")
BaseDir, err := os.Getwd()
if err != nil {
Logger.Fatal("Failed to get base directory", "err", err)
}
w := watcher.New()
for _, i := range Projects {
absPath, err := filepath.Abs(i)
if err != nil {
Logger.Warn("Failed to add project to watcher", "project", i, "err", err)
continue
}
err = w.Add(absPath)
if err != nil {
Logger.Warn("Failed to add project to watcher", "project", i, "err", err)
continue
}
ProjectPaths[absPath] = true
UpdateTemplate(absPath)
}
if !WatchMode {
Logger.Info("Build finished")
return
}
Logger.Info("Starting watch mode")
go UpdateOnChange(w.Event)
go func() {
err := w.Start(5 * time.Second)
Logger.Warn("Watcher stopped", "err", err)
}()
_ = http.ListenAndServe(":8080", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/" {
TemplateMu.RLock()
defer TemplateMu.RUnlock()
type ProjectData struct {
Name string
Files []string
}
projects := make([]ProjectData, 0)
for k, i := range TemplateMap {
files := make([]string, 0, len(i.Templates()))
for _, v2 := range i.Templates() {
if v2.Name() != "theme-template-root" {
files = append(files, v2.Name())
}
}
sort.Strings(files)
projects = append(projects, ProjectData{
Name: filepath.Base(k),
Files: files,
})
}
err := MainIndexTemplate.Execute(w, projects)
if err != nil {
Logger.Warn("Failed to render main-index.go.html", "err", err)
}
return
}
if r.URL.Path == "/favicon.ico" {
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
}
if r.URL.Path == "/style.css" {
parse, err := url.Parse(r.Referer())
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
project, _, ok := ParseProjectPath(parse.Path)
if !ok {
Logger.Warn("Invalid referrer path", "path", parse.Path)
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
}
http.ServeFile(w, r, filepath.Join(BaseDir, project, "style.css"))
return
}
if strings.Contains(r.URL.Path, "..") || strings.Contains(r.URL.RawPath, "..") {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
project, path, ok := ParseProjectPath(r.URL.Path)
if !ok {
Logger.Warn("Invalid path", "path", r.URL.Path)
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
}
p2 := filepath.Join(BaseDir, project)
TemplateMu.RLock()
defer TemplateMu.RUnlock()
if TemplateMap[p2] == nil {
Logger.Warn("Invalid project", "path", p2)
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
}
err = TemplateMap[p2].ExecuteTemplate(w, path, TemplateData[p2])
if err != nil {
Logger.Warn("Failed to render template", "path", p2, "name", path, "err", err)
}
}))
}
func ParseProjectPath(path string) (string, string, bool) {
p := path
if strings.HasPrefix(p, "/") {
p = p[1:]
}
n := strings.IndexByte(p, '/')
if n == -1 {
return "", "", false
}
return p[:n], p[n+1:], true
}
func UpdateOnChange(event <-chan watcher.Event) {
for i := range event {
Logger.Debug("Event", "op", i.Op, "name", i.FileInfo.Name(), "oldpath", i.OldPath, "path", i.Path)
if ProjectPaths[i.Path] {
UpdateTemplate(i.Path)
}
if ProjectPaths[i.OldPath] {
UpdateTemplate(i.OldPath)
}
}
}
func UpdateTemplate(p string) {
Logger.Info("Recompiling template", "path", p)
// run tailwind command
args := []string{"-i", filepath.Join(p, "main.css"), "-o", filepath.Join(p, "style.css"), "-c", filepath.Join(filepath.Dir(p), "tailwind.config.js"), "--minify"}
cmd := exec.Command("tailwindcss", args...)
cmd.Dir = p
tailwindOutput, err := cmd.CombinedOutput()
if err != nil {
Logger.Warn("Failed to run tailwind", "err", err)
return
}
Logger.Debug(string(tailwindOutput))
// make new template
fs, err := template.New("theme-template-root").Funcs(DefaultFuncMap).ParseFS(os.DirFS(p), "*.go.html")
if err != nil {
Logger.Warn("Failed to parse template", "err", err)
return
}
dataJson, err := os.Open(filepath.Join(p, "data.json"))
if err != nil {
Logger.Warn("Failed to open data.json", "err", err)
return
}
var dataBlob any
err = json.NewDecoder(dataJson).Decode(&dataBlob)
if err != nil {
Logger.Warn("Failed to parse data.json", "err", err)
return
}
TemplateMu.Lock()
TemplateMap[p] = fs
TemplateData[p] = dataBlob
TemplateMu.Unlock()
}
func EmailHide(a string) string {
b := []byte(a)
for i := range b {
if b[i] != '@' && b[i] != '.' {
b[i] = 'x'
}
}
return string(b)
}