mirror of
https://github.com/1f349/themes.git
synced 2024-11-09 22:32:48 +00:00
242 lines
5.6 KiB
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)
|
||
|
}
|