package webui
import (
"embed"
"errors"
"html/template"
"io"
"path"
"strings"
"time"
)
//go:embed templates/*.gohtml static/admin.css static/admin.js
var embeddedFiles embed.FS
type Renderer struct {
templates *template.Template
}
func NewRenderer() (*Renderer, error) {
templates, err := template.New("admin").
Option("missingkey=error").
Funcs(template.FuncMap{
"displayTime": displayTime,
"machineTime": machineTime,
"pathPart": pathPart,
}).
ParseFS(embeddedFiles, "templates/*.gohtml")
if err != nil {
return nil, errors.New("parse admin templates")
}
return &Renderer{templates: templates}, nil
}
func (r *Renderer) Execute(
writer io.Writer,
name string,
data any,
) error {
if r == nil || r.templates == nil {
return errors.New("admin renderer is not configured")
}
if err := r.templates.ExecuteTemplate(writer, name, data); err != nil {
return errors.New("render admin template")
}
return nil
}
func staticFile(name string) ([]byte, error) {
if name != "admin.css" && name != "admin.js" {
return nil, errors.New("unknown admin static file")
}
content, err := embeddedFiles.ReadFile("static/" + name)
if err != nil {
return nil, errors.New("read admin static file")
}
return content, nil
}
func displayTime(value time.Time) string {
if value.IsZero() {
return "尚未记录"
}
return value.Local().Format("2006-01-02 15:04")
}
func machineTime(value time.Time) string {
if value.IsZero() {
return ""
}
return value.UTC().Format(time.RFC3339)
}
func pathPart(value string) string {
return pathEscape(strings.TrimSpace(value))
}
func pathEscape(value string) string {
// Keep IDs as one opaque path component.
if value == "" || value == "." || value == ".." ||
path.Base(value) != value || strings.ContainsAny(value, `/\`) {
return "invalid"
}
return value
}