package web import ( "fmt" "html/template" "net/url" "strings" "time" ) func templateFuncs() template.FuncMap { return template.FuncMap{ "repoURL": repoURL, "manifestURL": manifestURL, "pull": pullAddress, "size": formatSize, "timestamp": timestamp, "age": displayAge, "join": strings.Join, } } // sanitizeURLPath escapes each path segment separately, preserving slashes in // names such as group/repository. Appends a trailing slash. func sanitizeURLPath(path string) string { parts := strings.Split(path, "/") for i := range parts { parts[i] = url.PathEscape(parts[i]) } return strings.Join(parts, "/") + "/" } func repoURL(name string) string { return "/r/" + sanitizeURLPath(name) } func manifestURL(repository, digest, tag string) string { path := repoURL(repository) + "m/" + sanitizeURLPath(digest) if tag != "" { path += "?" + url.Values{"tag": {tag}}.Encode() } return path } func pullAddress(host, repository, tag, digest string) string { address := strings.TrimSuffix(host, "/") + "/" + repository if tag != "" { address += ":" + tag } if digest != "" { address += "@" + digest } return address } func formatSize(value int64) string { if value >= 1024*1024 { return fmt.Sprintf("%.1fMB", float64(value)/(1024*1024)) } if value >= 1024 { return fmt.Sprintf("%.1fKB", float64(value)/1024) } return fmt.Sprintf("%dB", value) } func timestamp(t time.Time) string { if t.IsZero() { return "" } return t.Format(time.DateTime + " MST") } func displayAge(updated time.Time, frozen string) string { if frozen != "" { return frozen } if updated.IsZero() { return "unknown" } elapsed := time.Since(updated) if elapsed < time.Minute { return "just now" } for _, unit := range []struct { duration time.Duration label string }{ {365 * 24 * time.Hour, "year"}, {30 * 24 * time.Hour, "month"}, {24 * time.Hour, "day"}, {time.Hour, "hour"}, {time.Minute, "minute"}, } { if count := int(elapsed / unit.duration); count > 0 { suffix := "s" if count == 1 { suffix = "" } return fmt.Sprintf("%d %s%s ago", count, unit.label, suffix) } } return "just now" }