mirror of
https://github.com/hanzoai/forge.git
synced 2026-10-30 15:00:39 +00:00
Source files keep their SPDX-License-Identifier and Hanzo copyright line; the Gitea, Gogs and LFS Test Server copyrights they used to repeat now live once in NOTICE, alongside LICENSE. The header lint accepts any copyright line above the SPDX line, so it no longer demands an upstream one. Co-authored-by: Hanzo Dev <[email protected]>
134 lines
3.8 KiB
Go
134 lines
3.8 KiB
Go
// Copyright 2026 Hanzo AI, Inc. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package templates
|
|
|
|
import (
|
|
"fmt"
|
|
"html"
|
|
"html/template"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/hanzoai/git/modules/setting"
|
|
"github.com/hanzoai/git/modules/timeutil"
|
|
)
|
|
|
|
type DateUtils struct{}
|
|
|
|
func NewDateUtils() *DateUtils {
|
|
return (*DateUtils)(nil) // the util is stateless, and we do not need to create an instance
|
|
}
|
|
|
|
// AbsoluteShort renders in "Jan 01, 2006" format
|
|
func (du *DateUtils) AbsoluteShort(time any) template.HTML {
|
|
return dateTimeFormat("short", time)
|
|
}
|
|
|
|
// AbsoluteLong renders in "January 01, 2006" format
|
|
func (du *DateUtils) AbsoluteLong(time any) template.HTML {
|
|
return dateTimeFormat("long", time)
|
|
}
|
|
|
|
// FullTime renders in "Jan 01, 2006 20:33:44" format
|
|
func (du *DateUtils) FullTime(time any) template.HTML {
|
|
return dateTimeFormat("full", time)
|
|
}
|
|
|
|
func (du *DateUtils) TimeSince(time any) template.HTML {
|
|
return TimeSince(time)
|
|
}
|
|
|
|
// ParseLegacy parses the datetime in legacy format, eg: "2016-01-02" in server's timezone.
|
|
// It shouldn't be used in new code. New code should use Time or TimeStamp as much as possible.
|
|
func (du *DateUtils) ParseLegacy(datetime string) time.Time {
|
|
return parseLegacy(datetime)
|
|
}
|
|
|
|
func parseLegacy(datetime string) time.Time {
|
|
t, err := time.Parse(time.RFC3339, datetime)
|
|
if err != nil {
|
|
t, _ = time.ParseInLocation(time.DateOnly, datetime, setting.DefaultUILocation)
|
|
}
|
|
return t
|
|
}
|
|
|
|
func anyToTime(value any) (t time.Time, isZero bool) {
|
|
switch v := value.(type) {
|
|
case nil:
|
|
// it is zero
|
|
case *time.Time:
|
|
if v != nil {
|
|
t = *v
|
|
}
|
|
case time.Time:
|
|
t = v
|
|
case timeutil.TimeStamp:
|
|
t = v.AsTime()
|
|
case timeutil.TimeStampNano:
|
|
t = v.AsTime()
|
|
case int:
|
|
t = timeutil.TimeStamp(v).AsTime()
|
|
case int64:
|
|
t = timeutil.TimeStamp(v).AsTime()
|
|
default:
|
|
panic(fmt.Sprintf("Unsupported time type %T", value))
|
|
}
|
|
return t, t.IsZero() || t.Unix() == 0
|
|
}
|
|
|
|
func dateTimeFormat(format string, datetime any) template.HTML {
|
|
t, isZero := anyToTime(datetime)
|
|
if isZero {
|
|
return "-"
|
|
}
|
|
var textEscaped string
|
|
datetimeEscaped := html.EscapeString(t.Format(time.RFC3339))
|
|
if format == "full" {
|
|
textEscaped = html.EscapeString(t.Format("2006-01-02 15:04:05 -07:00"))
|
|
} else {
|
|
textEscaped = html.EscapeString(t.Format("2006-01-02"))
|
|
}
|
|
|
|
attrs := []string{`weekday=""`, `year="numeric"`}
|
|
switch format {
|
|
case "short", "long": // date only
|
|
attrs = append(attrs, `threshold="P0Y"`, `month="`+format+`"`, `day="numeric"`, `prefix=""`)
|
|
case "full": // full date including time
|
|
attrs = append(attrs, `format="datetime"`, `month="short"`, `day="numeric"`, `hour="numeric"`, `minute="numeric"`, `second="numeric"`, `data-tooltip-content`, `data-tooltip-interactive="true"`)
|
|
default:
|
|
panic("Unsupported format " + format)
|
|
}
|
|
|
|
return template.HTML(fmt.Sprintf(`<relative-time %s datetime="%s">%s</relative-time>`, strings.Join(attrs, " "), datetimeEscaped, textEscaped))
|
|
}
|
|
|
|
func timeSinceTo(then any, now time.Time) template.HTML {
|
|
thenTime, isZero := anyToTime(then)
|
|
if isZero {
|
|
return "-"
|
|
}
|
|
|
|
friendlyText := thenTime.Format("2006-01-02 15:04:05 -07:00")
|
|
|
|
// document: https://github.com/github/relative-time-element
|
|
attrs := `tense="past"`
|
|
isFuture := now.Before(thenTime)
|
|
if isFuture {
|
|
attrs = `tense="future"`
|
|
}
|
|
|
|
// declare data-tooltip-content attribute to switch from "title" tooltip to "tippy" tooltip
|
|
htm := fmt.Sprintf(`<relative-time prefix="" %s datetime="%s" data-tooltip-content data-tooltip-interactive="true">%s</relative-time>`,
|
|
attrs, thenTime.Format(time.RFC3339), friendlyText)
|
|
return template.HTML(htm)
|
|
}
|
|
|
|
// TimeSince renders relative time HTML given a time
|
|
func TimeSince(then any) template.HTML {
|
|
if setting.UI.PreferredTimestampTense == "absolute" {
|
|
return dateTimeFormat("full", then)
|
|
}
|
|
return timeSinceTo(then, time.Now())
|
|
}
|