Refactor html template handling
This commit is contained in:
parent
41a2c5472b
commit
a200f5cc83
299
server/html_templates.go
Normal file
299
server/html_templates.go
Normal file
@ -0,0 +1,299 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"runtime/debug"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/golang/glog"
|
||||
"github.com/trezor/blockbook/api"
|
||||
"github.com/trezor/blockbook/common"
|
||||
)
|
||||
|
||||
type tpl int
|
||||
|
||||
const (
|
||||
noTpl = tpl(iota)
|
||||
errorTpl
|
||||
errorInternalTpl
|
||||
)
|
||||
|
||||
// htmlTemplateHandler is a handle to public http server
|
||||
type htmlTemplates[TD any] struct {
|
||||
metrics *common.Metrics
|
||||
templates []*template.Template
|
||||
debug bool
|
||||
newTemplateData func(r *http.Request) *TD
|
||||
newTemplateDataWithError func(error *api.APIError, r *http.Request) *TD
|
||||
parseTemplates func() []*template.Template
|
||||
postHtmlTemplateHandler func(data *TD, w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
func (s *htmlTemplates[TD]) htmlTemplateHandler(handler func(w http.ResponseWriter, r *http.Request) (tpl, *TD, error)) func(w http.ResponseWriter, r *http.Request) {
|
||||
handlerName := getFunctionName(handler)
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var t tpl
|
||||
var data *TD
|
||||
var err error
|
||||
defer func() {
|
||||
if e := recover(); e != nil {
|
||||
glog.Error(handlerName, " recovered from panic: ", e)
|
||||
debug.PrintStack()
|
||||
t = errorInternalTpl
|
||||
if s.debug {
|
||||
data = s.newTemplateDataWithError(&api.APIError{Text: fmt.Sprint("Internal server error: recovered from panic ", e)}, r)
|
||||
} else {
|
||||
data = s.newTemplateDataWithError(&api.APIError{Text: "Internal server error"}, r)
|
||||
}
|
||||
}
|
||||
// noTpl means the handler completely handled the request
|
||||
if t != noTpl {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
// return 500 Internal Server Error with errorInternalTpl
|
||||
if t == errorInternalTpl {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
if err := s.templates[t].ExecuteTemplate(w, "base.html", data); err != nil {
|
||||
glog.Error(err)
|
||||
}
|
||||
}
|
||||
if s.metrics != nil {
|
||||
s.metrics.ExplorerPendingRequests.With((common.Labels{"method": handlerName})).Dec()
|
||||
}
|
||||
}()
|
||||
if s.metrics != nil {
|
||||
s.metrics.ExplorerPendingRequests.With((common.Labels{"method": handlerName})).Inc()
|
||||
}
|
||||
if s.debug {
|
||||
// reload templates on each request
|
||||
// to reflect changes during development
|
||||
s.templates = s.parseTemplates()
|
||||
}
|
||||
t, data, err = handler(w, r)
|
||||
if err != nil || (data == nil && t != noTpl) {
|
||||
t = errorInternalTpl
|
||||
if apiErr, ok := err.(*api.APIError); ok {
|
||||
data = s.newTemplateDataWithError(apiErr, r)
|
||||
if apiErr.Public {
|
||||
t = errorTpl
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
glog.Error(handlerName, " error: ", err)
|
||||
}
|
||||
if s.debug {
|
||||
data = s.newTemplateDataWithError(&api.APIError{Text: fmt.Sprintf("Internal server error: %v, data %+v", err, data)}, r)
|
||||
} else {
|
||||
data = s.newTemplateDataWithError(&api.APIError{Text: "Internal server error"}, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
if s.postHtmlTemplateHandler != nil {
|
||||
s.postHtmlTemplateHandler(data, w, r)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func relativeTimeUnit(d int64) string {
|
||||
var u string
|
||||
if d < 60 {
|
||||
if d == 1 {
|
||||
u = " sec"
|
||||
} else {
|
||||
u = " secs"
|
||||
}
|
||||
} else if d < 3600 {
|
||||
d /= 60
|
||||
if d == 1 {
|
||||
u = " min"
|
||||
} else {
|
||||
u = " mins"
|
||||
}
|
||||
} else if d < 3600*24 {
|
||||
d /= 3600
|
||||
if d == 1 {
|
||||
u = " hour"
|
||||
} else {
|
||||
u = " hours"
|
||||
}
|
||||
} else {
|
||||
d /= 3600 * 24
|
||||
if d == 1 {
|
||||
u = " day"
|
||||
} else {
|
||||
u = " days"
|
||||
}
|
||||
}
|
||||
return strconv.FormatInt(d, 10) + u
|
||||
}
|
||||
|
||||
func relativeTime(d int64) string {
|
||||
r := relativeTimeUnit(d)
|
||||
if d > 3600*24 {
|
||||
d = d % (3600 * 24)
|
||||
if d >= 3600 {
|
||||
r += " " + relativeTimeUnit(d)
|
||||
}
|
||||
} else if d > 3600 {
|
||||
d = d % 3600
|
||||
if d >= 60 {
|
||||
r += " " + relativeTimeUnit(d)
|
||||
}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func unixTimeSpan(ut int64) template.HTML {
|
||||
t := time.Unix(ut, 0)
|
||||
return timeSpan(&t)
|
||||
}
|
||||
|
||||
var timeNow = time.Now
|
||||
|
||||
func timeSpan(t *time.Time) template.HTML {
|
||||
if t == nil {
|
||||
return ""
|
||||
}
|
||||
u := t.Unix()
|
||||
if u <= 0 {
|
||||
return ""
|
||||
}
|
||||
d := timeNow().Unix() - u
|
||||
f := t.UTC().Format("2006-01-02 15:04:05")
|
||||
if d < 0 {
|
||||
return template.HTML(f)
|
||||
}
|
||||
r := relativeTime(d)
|
||||
return template.HTML(`<span tt="` + f + `">` + r + " ago</span>")
|
||||
}
|
||||
|
||||
func toJSON(data interface{}) string {
|
||||
json, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return string(json)
|
||||
}
|
||||
|
||||
func formatAmountWithDecimals(a *api.Amount, d int) string {
|
||||
if a == nil {
|
||||
return "0"
|
||||
}
|
||||
return a.DecimalString(d)
|
||||
}
|
||||
|
||||
func appendAmountSpan(rv *strings.Builder, class, amount, shortcut, txDate string) {
|
||||
rv.WriteString(`<span`)
|
||||
if class != "" {
|
||||
rv.WriteString(` class="`)
|
||||
rv.WriteString(class)
|
||||
rv.WriteString(`"`)
|
||||
}
|
||||
if txDate != "" {
|
||||
rv.WriteString(` tm="`)
|
||||
rv.WriteString(txDate)
|
||||
rv.WriteString(`"`)
|
||||
}
|
||||
rv.WriteString(">")
|
||||
i := strings.IndexByte(amount, '.')
|
||||
if i < 0 {
|
||||
appendSeparatedNumberSpans(rv, amount, "nc")
|
||||
} else {
|
||||
appendSeparatedNumberSpans(rv, amount[:i], "nc")
|
||||
rv.WriteString(`.`)
|
||||
rv.WriteString(`<span class="amt-dec">`)
|
||||
appendLeftSeparatedNumberSpans(rv, amount[i+1:], "ns")
|
||||
rv.WriteString("</span>")
|
||||
}
|
||||
if shortcut != "" {
|
||||
rv.WriteString(" ")
|
||||
rv.WriteString(shortcut)
|
||||
}
|
||||
rv.WriteString("</span>")
|
||||
}
|
||||
|
||||
func appendAmountWrapperSpan(rv *strings.Builder, primary, symbol, classes string) {
|
||||
rv.WriteString(`<span class="amt`)
|
||||
if classes != "" {
|
||||
rv.WriteString(` `)
|
||||
rv.WriteString(classes)
|
||||
}
|
||||
rv.WriteString(`" cc="`)
|
||||
rv.WriteString(primary)
|
||||
rv.WriteString(" ")
|
||||
rv.WriteString(symbol)
|
||||
rv.WriteString(`">`)
|
||||
}
|
||||
|
||||
func formatInt(i int) template.HTML {
|
||||
return formatInt64(int64(i))
|
||||
}
|
||||
|
||||
func formatUint32(i uint32) template.HTML {
|
||||
return formatInt64(int64(i))
|
||||
}
|
||||
|
||||
func appendSeparatedNumberSpans(rv *strings.Builder, s, separatorClass string) {
|
||||
if len(s) > 0 && s[0] == '-' {
|
||||
s = s[1:]
|
||||
rv.WriteByte('-')
|
||||
}
|
||||
t := (len(s) - 1) / 3
|
||||
if t <= 0 {
|
||||
rv.WriteString(s)
|
||||
} else {
|
||||
t *= 3
|
||||
rv.WriteString(s[:len(s)-t])
|
||||
for i := len(s) - t; i < len(s); i += 3 {
|
||||
rv.WriteString(`<span class="`)
|
||||
rv.WriteString(separatorClass)
|
||||
rv.WriteString(`">`)
|
||||
rv.WriteString(s[i : i+3])
|
||||
rv.WriteString("</span>")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func appendLeftSeparatedNumberSpans(rv *strings.Builder, s, separatorClass string) {
|
||||
l := len(s)
|
||||
if l <= 3 {
|
||||
rv.WriteString(s)
|
||||
} else {
|
||||
rv.WriteString(s[:3])
|
||||
for i := 3; i < len(s); i += 3 {
|
||||
rv.WriteString(`<span class="`)
|
||||
rv.WriteString(separatorClass)
|
||||
rv.WriteString(`">`)
|
||||
e := i + 3
|
||||
if e > l {
|
||||
e = l
|
||||
}
|
||||
rv.WriteString(s[i:e])
|
||||
rv.WriteString("</span>")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func formatInt64(i int64) template.HTML {
|
||||
s := strconv.FormatInt(i, 10)
|
||||
var rv strings.Builder
|
||||
appendSeparatedNumberSpans(&rv, s, "ns")
|
||||
return template.HTML(rv.String())
|
||||
}
|
||||
|
||||
func formatBigInt(i *big.Int) template.HTML {
|
||||
if i == nil {
|
||||
return ""
|
||||
}
|
||||
s := i.String()
|
||||
var rv strings.Builder
|
||||
appendSeparatedNumberSpans(&rv, s, "ns")
|
||||
return template.HTML(rv.String())
|
||||
}
|
||||
173
server/html_templates_test.go
Normal file
173
server/html_templates_test.go
Normal file
@ -0,0 +1,173 @@
|
||||
//go:build unittest
|
||||
|
||||
package server
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func Test_formatInt64(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
n int64
|
||||
want template.HTML
|
||||
}{
|
||||
{"1", 1, "1"},
|
||||
{"13", 13, "13"},
|
||||
{"123", 123, "123"},
|
||||
{"1234", 1234, `1<span class="ns">234</span>`},
|
||||
{"91234", 91234, `91<span class="ns">234</span>`},
|
||||
{"891234", 891234, `891<span class="ns">234</span>`},
|
||||
{"7891234", 7891234, `7<span class="ns">891</span><span class="ns">234</span>`},
|
||||
{"67891234", 67891234, `67<span class="ns">891</span><span class="ns">234</span>`},
|
||||
{"567891234", 567891234, `567<span class="ns">891</span><span class="ns">234</span>`},
|
||||
{"4567891234", 4567891234, `4<span class="ns">567</span><span class="ns">891</span><span class="ns">234</span>`},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := formatInt64(tt.n); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("formatInt64() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_formatTime(t *testing.T) {
|
||||
timeNow = fixedTimeNow
|
||||
tests := []struct {
|
||||
name string
|
||||
want template.HTML
|
||||
}{
|
||||
{
|
||||
name: "2020-12-23 15:16:17",
|
||||
want: `<span tt="2020-12-23 15:16:17">630 days 21 hours ago</span>`,
|
||||
},
|
||||
{
|
||||
name: "2022-08-23 11:12:13",
|
||||
want: `<span tt="2022-08-23 11:12:13">23 days 1 hour ago</span>`,
|
||||
},
|
||||
{
|
||||
name: "2022-09-14 11:12:13",
|
||||
want: `<span tt="2022-09-14 11:12:13">1 day 1 hour ago</span>`,
|
||||
},
|
||||
{
|
||||
name: "2022-09-14 14:12:13",
|
||||
want: `<span tt="2022-09-14 14:12:13">22 hours 31 mins ago</span>`,
|
||||
},
|
||||
{
|
||||
name: "2022-09-15 09:33:26",
|
||||
want: `<span tt="2022-09-15 09:33:26">3 hours 10 mins ago</span>`,
|
||||
},
|
||||
{
|
||||
name: "2022-09-15 12:23:56",
|
||||
want: `<span tt="2022-09-15 12:23:56">20 mins ago</span>`,
|
||||
},
|
||||
{
|
||||
name: "2022-09-15 12:24:07",
|
||||
want: `<span tt="2022-09-15 12:24:07">19 mins ago</span>`,
|
||||
},
|
||||
{
|
||||
name: "2022-09-15 12:43:21",
|
||||
want: `<span tt="2022-09-15 12:43:21">35 secs ago</span>`,
|
||||
},
|
||||
{
|
||||
name: "2022-09-15 12:43:56",
|
||||
want: `<span tt="2022-09-15 12:43:56">0 secs ago</span>`,
|
||||
},
|
||||
{
|
||||
name: "2022-09-16 12:43:56",
|
||||
want: `2022-09-16 12:43:56`,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tm, _ := time.Parse("2006-01-02 15:04:05", tt.name)
|
||||
if got := timeSpan(&tm); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("formatTime() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_appendAmountSpan(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
class string
|
||||
amount string
|
||||
shortcut string
|
||||
txDate string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "prim-amt 1.23456789 BTC",
|
||||
class: "prim-amt",
|
||||
amount: "1.23456789",
|
||||
shortcut: "BTC",
|
||||
want: `<span class="prim-amt">1.<span class="amt-dec">234<span class="ns">567</span><span class="ns">89</span></span> BTC</span>`,
|
||||
},
|
||||
{
|
||||
name: "prim-amt 1432134.23456 BTC",
|
||||
class: "prim-amt",
|
||||
amount: "1432134.23456",
|
||||
shortcut: "BTC",
|
||||
want: `<span class="prim-amt">1<span class="nc">432</span><span class="nc">134</span>.<span class="amt-dec">234<span class="ns">56</span></span> BTC</span>`,
|
||||
},
|
||||
{
|
||||
name: "sec-amt 1 EUR",
|
||||
class: "sec-amt",
|
||||
amount: "1",
|
||||
shortcut: "EUR",
|
||||
want: `<span class="sec-amt">1 EUR</span>`,
|
||||
},
|
||||
{
|
||||
name: "sec-amt -1 EUR",
|
||||
class: "sec-amt",
|
||||
amount: "-1",
|
||||
shortcut: "EUR",
|
||||
want: `<span class="sec-amt">-1 EUR</span>`,
|
||||
},
|
||||
{
|
||||
name: "sec-amt 432109.23 EUR",
|
||||
class: "sec-amt",
|
||||
amount: "432109.23",
|
||||
shortcut: "EUR",
|
||||
want: `<span class="sec-amt">432<span class="nc">109</span>.<span class="amt-dec">23</span> EUR</span>`,
|
||||
},
|
||||
{
|
||||
name: "sec-amt -432109.23 EUR",
|
||||
class: "sec-amt",
|
||||
amount: "-432109.23",
|
||||
shortcut: "EUR",
|
||||
want: `<span class="sec-amt">-432<span class="nc">109</span>.<span class="amt-dec">23</span> EUR</span>`,
|
||||
},
|
||||
{
|
||||
name: "sec-amt 43141.29 EUR",
|
||||
class: "sec-amt",
|
||||
amount: "43141.29",
|
||||
shortcut: "EUR",
|
||||
txDate: "2022-03-14",
|
||||
want: `<span class="sec-amt" tm="2022-03-14">43<span class="nc">141</span>.<span class="amt-dec">29</span> EUR</span>`,
|
||||
},
|
||||
{
|
||||
name: "sec-amt -43141.29 EUR",
|
||||
class: "sec-amt",
|
||||
amount: "-43141.29",
|
||||
shortcut: "EUR",
|
||||
txDate: "2022-03-14",
|
||||
want: `<span class="sec-amt" tm="2022-03-14">-43<span class="nc">141</span>.<span class="amt-dec">29</span> EUR</span>`,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var rv strings.Builder
|
||||
appendAmountSpan(&rv, tt.class, tt.amount, tt.shortcut, tt.txDate)
|
||||
if got := rv.String(); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("formatTime() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
299
server/public.go
299
server/public.go
@ -40,8 +40,9 @@ const (
|
||||
apiV2
|
||||
)
|
||||
|
||||
// PublicServer is a handle to public http server
|
||||
// PublicServer provides public http server functionality
|
||||
type PublicServer struct {
|
||||
htmlTemplates[TemplateData]
|
||||
binding string
|
||||
certFiles string
|
||||
socketio *SocketIoServer
|
||||
@ -55,10 +56,7 @@ type PublicServer struct {
|
||||
api *api.Worker
|
||||
explorerURL string
|
||||
internalExplorer bool
|
||||
metrics *common.Metrics
|
||||
is *common.InternalState
|
||||
templates []*template.Template
|
||||
debug bool
|
||||
}
|
||||
|
||||
// NewPublicServer creates new public server http interface to blockbook and returns its handle
|
||||
@ -88,6 +86,10 @@ func NewPublicServer(binding string, certFiles string, db *db.RocksDB, chain bch
|
||||
}
|
||||
|
||||
s := &PublicServer{
|
||||
htmlTemplates: htmlTemplates[TemplateData]{
|
||||
metrics: metrics,
|
||||
debug: debugMode,
|
||||
},
|
||||
binding: binding,
|
||||
certFiles: certFiles,
|
||||
https: https,
|
||||
@ -101,10 +103,12 @@ func NewPublicServer(binding string, certFiles string, db *db.RocksDB, chain bch
|
||||
mempool: mempool,
|
||||
explorerURL: explorerURL,
|
||||
internalExplorer: explorerURL == "",
|
||||
metrics: metrics,
|
||||
is: is,
|
||||
debug: debugMode,
|
||||
}
|
||||
s.htmlTemplates.newTemplateData = s.newTemplateData
|
||||
s.htmlTemplates.newTemplateDataWithError = s.newTemplateDataWithError
|
||||
s.htmlTemplates.parseTemplates = s.parseTemplates
|
||||
s.htmlTemplates.postHtmlTemplateHandler = s.postHtmlTemplateHandler
|
||||
s.templates = s.parseTemplates()
|
||||
|
||||
// map only basic functions, the rest is enabled by method MapFullPublicInterface
|
||||
@ -396,82 +400,14 @@ func (s *PublicServer) newTemplateData(r *http.Request) *TemplateData {
|
||||
return t
|
||||
}
|
||||
|
||||
func (s *PublicServer) newTemplateDataWithError(text string, r *http.Request) *TemplateData {
|
||||
func (s *PublicServer) newTemplateDataWithError(error *api.APIError, r *http.Request) *TemplateData {
|
||||
td := s.newTemplateData(r)
|
||||
td.Error = &api.APIError{Text: text}
|
||||
td.Error = error
|
||||
return td
|
||||
}
|
||||
|
||||
func (s *PublicServer) htmlTemplateHandler(handler func(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error)) func(w http.ResponseWriter, r *http.Request) {
|
||||
handlerName := getFunctionName(handler)
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var t tpl
|
||||
var data *TemplateData
|
||||
var err error
|
||||
defer func() {
|
||||
if e := recover(); e != nil {
|
||||
glog.Error(handlerName, " recovered from panic: ", e)
|
||||
debug.PrintStack()
|
||||
t = errorInternalTpl
|
||||
if s.debug {
|
||||
data = s.newTemplateDataWithError(fmt.Sprint("Internal server error: recovered from panic ", e), r)
|
||||
} else {
|
||||
data = s.newTemplateDataWithError("Internal server error", r)
|
||||
}
|
||||
}
|
||||
// noTpl means the handler completely handled the request
|
||||
if t != noTpl {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
// return 500 Internal Server Error with errorInternalTpl
|
||||
if t == errorInternalTpl {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
if err := s.templates[t].ExecuteTemplate(w, "base.html", data); err != nil {
|
||||
glog.Error(err)
|
||||
}
|
||||
}
|
||||
s.metrics.ExplorerPendingRequests.With((common.Labels{"method": handlerName})).Dec()
|
||||
}()
|
||||
s.metrics.ExplorerPendingRequests.With((common.Labels{"method": handlerName})).Inc()
|
||||
if s.debug {
|
||||
// reload templates on each request
|
||||
// to reflect changes during development
|
||||
s.templates = s.parseTemplates()
|
||||
}
|
||||
t, data, err = handler(w, r)
|
||||
if err != nil || (data == nil && t != noTpl) {
|
||||
t = errorInternalTpl
|
||||
if apiErr, ok := err.(*api.APIError); ok {
|
||||
data = s.newTemplateData(r)
|
||||
data.Error = apiErr
|
||||
if apiErr.Public {
|
||||
t = errorTpl
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
glog.Error(handlerName, " error: ", err)
|
||||
}
|
||||
if s.debug {
|
||||
data = s.newTemplateDataWithError(fmt.Sprintf("Internal server error: %v, data %+v", err, data), r)
|
||||
} else {
|
||||
data = s.newTemplateDataWithError("Internal server error", r)
|
||||
}
|
||||
}
|
||||
}
|
||||
// if SecondaryCoin is specified, set secondary_coin cookie
|
||||
if data != nil && data.SecondaryCoin != "" {
|
||||
http.SetCookie(w, &http.Cookie{Name: secondaryCoinCookieName, Value: data.SecondaryCoin + "=" + strconv.FormatBool(data.UseSecondaryCoin), Path: "/"})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type tpl int
|
||||
|
||||
const (
|
||||
noTpl = tpl(iota)
|
||||
errorTpl
|
||||
errorInternalTpl
|
||||
indexTpl
|
||||
indexTpl = iota + errorInternalTpl + 1
|
||||
txTpl
|
||||
addressTpl
|
||||
xpubTpl
|
||||
@ -481,7 +417,7 @@ const (
|
||||
mempoolTpl
|
||||
nftDetailTpl
|
||||
|
||||
tplCount
|
||||
publicTplCount
|
||||
)
|
||||
|
||||
// TemplateData is used to transfer data to the templates
|
||||
@ -590,7 +526,7 @@ func (s *PublicServer) parseTemplates() []*template.Template {
|
||||
return t
|
||||
}
|
||||
}
|
||||
t := make([]*template.Template, tplCount)
|
||||
t := make([]*template.Template, publicTplCount)
|
||||
t[errorTpl] = createTemplate("./static/templates/error.html", "./static/templates/base.html")
|
||||
t[errorInternalTpl] = createTemplate("./static/templates/error.html", "./static/templates/base.html")
|
||||
t[indexTpl] = createTemplate("./static/templates/index.html", "./static/templates/base.html")
|
||||
@ -611,85 +547,12 @@ func (s *PublicServer) parseTemplates() []*template.Template {
|
||||
return t
|
||||
}
|
||||
|
||||
func relativeTimeUnit(d int64) string {
|
||||
var u string
|
||||
if d < 60 {
|
||||
if d == 1 {
|
||||
u = " sec"
|
||||
} else {
|
||||
u = " secs"
|
||||
}
|
||||
} else if d < 3600 {
|
||||
d /= 60
|
||||
if d == 1 {
|
||||
u = " min"
|
||||
} else {
|
||||
u = " mins"
|
||||
}
|
||||
} else if d < 3600*24 {
|
||||
d /= 3600
|
||||
if d == 1 {
|
||||
u = " hour"
|
||||
} else {
|
||||
u = " hours"
|
||||
}
|
||||
} else {
|
||||
d /= 3600 * 24
|
||||
if d == 1 {
|
||||
u = " day"
|
||||
} else {
|
||||
u = " days"
|
||||
}
|
||||
func (s *PublicServer) postHtmlTemplateHandler(data *TemplateData, w http.ResponseWriter, r *http.Request) {
|
||||
// // if SecondaryCoin is specified, set secondary_coin cookie
|
||||
if data != nil && data.SecondaryCoin != "" {
|
||||
http.SetCookie(w, &http.Cookie{Name: secondaryCoinCookieName, Value: data.SecondaryCoin + "=" + strconv.FormatBool(data.UseSecondaryCoin), Path: "/"})
|
||||
}
|
||||
return strconv.FormatInt(d, 10) + u
|
||||
}
|
||||
|
||||
func relativeTime(d int64) string {
|
||||
r := relativeTimeUnit(d)
|
||||
if d > 3600*24 {
|
||||
d = d % (3600 * 24)
|
||||
if d >= 3600 {
|
||||
r += " " + relativeTimeUnit(d)
|
||||
}
|
||||
} else if d > 3600 {
|
||||
d = d % 3600
|
||||
if d >= 60 {
|
||||
r += " " + relativeTimeUnit(d)
|
||||
}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func unixTimeSpan(ut int64) template.HTML {
|
||||
t := time.Unix(ut, 0)
|
||||
return timeSpan(&t)
|
||||
}
|
||||
|
||||
var timeNow = time.Now
|
||||
|
||||
func timeSpan(t *time.Time) template.HTML {
|
||||
if t == nil {
|
||||
return ""
|
||||
}
|
||||
u := t.Unix()
|
||||
if u <= 0 {
|
||||
return ""
|
||||
}
|
||||
d := timeNow().Unix() - u
|
||||
f := t.UTC().Format("2006-01-02 15:04:05")
|
||||
if d < 0 {
|
||||
return template.HTML(f)
|
||||
}
|
||||
r := relativeTime(d)
|
||||
return template.HTML(`<span tt="` + f + `">` + r + " ago</span>")
|
||||
}
|
||||
|
||||
func toJSON(data interface{}) string {
|
||||
json, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return string(json)
|
||||
}
|
||||
|
||||
func (s *PublicServer) formatAmount(a *api.Amount) string {
|
||||
@ -699,63 +562,6 @@ func (s *PublicServer) formatAmount(a *api.Amount) string {
|
||||
return s.chainParser.AmountToDecimalString((*big.Int)(a))
|
||||
}
|
||||
|
||||
func formatAmountWithDecimals(a *api.Amount, d int) string {
|
||||
if a == nil {
|
||||
return "0"
|
||||
}
|
||||
return a.DecimalString(d)
|
||||
}
|
||||
|
||||
func appendAmountSpan(rv *strings.Builder, class, amount, shortcut, txDate string) {
|
||||
rv.WriteString(`<span`)
|
||||
if class != "" {
|
||||
rv.WriteString(` class="`)
|
||||
rv.WriteString(class)
|
||||
rv.WriteString(`"`)
|
||||
}
|
||||
if txDate != "" {
|
||||
rv.WriteString(` tm="`)
|
||||
rv.WriteString(txDate)
|
||||
rv.WriteString(`"`)
|
||||
}
|
||||
rv.WriteString(">")
|
||||
i := strings.IndexByte(amount, '.')
|
||||
if i < 0 {
|
||||
appendSeparatedNumberSpans(rv, amount, "nc")
|
||||
} else {
|
||||
appendSeparatedNumberSpans(rv, amount[:i], "nc")
|
||||
rv.WriteString(`.`)
|
||||
rv.WriteString(`<span class="amt-dec">`)
|
||||
appendLeftSeparatedNumberSpans(rv, amount[i+1:], "ns")
|
||||
rv.WriteString("</span>")
|
||||
}
|
||||
if shortcut != "" {
|
||||
rv.WriteString(" ")
|
||||
rv.WriteString(shortcut)
|
||||
}
|
||||
rv.WriteString("</span>")
|
||||
}
|
||||
|
||||
func appendAmountWrapperSpan(rv *strings.Builder, primary, symbol, classes string) {
|
||||
rv.WriteString(`<span class="amt`)
|
||||
if classes != "" {
|
||||
rv.WriteString(` `)
|
||||
rv.WriteString(classes)
|
||||
}
|
||||
rv.WriteString(`" cc="`)
|
||||
rv.WriteString(primary)
|
||||
rv.WriteString(" ")
|
||||
rv.WriteString(symbol)
|
||||
rv.WriteString(`">`)
|
||||
}
|
||||
|
||||
func formatSecondaryAmount(a float64, td *TemplateData) string {
|
||||
if td.SecondaryCoin == "BTC" || td.SecondaryCoin == "ETH" {
|
||||
return strconv.FormatFloat(a, 'f', 6, 64)
|
||||
}
|
||||
return strconv.FormatFloat(a, 'f', 2, 64)
|
||||
}
|
||||
|
||||
func (s *PublicServer) amountSpan(a *api.Amount, td *TemplateData, classes string) template.HTML {
|
||||
primary := s.formatAmount(a)
|
||||
var rv strings.Builder
|
||||
@ -898,70 +704,11 @@ func (s *PublicServer) summaryValuesSpan(baseValue float64, secondaryValue float
|
||||
return template.HTML(rv.String())
|
||||
}
|
||||
|
||||
func formatInt(i int) template.HTML {
|
||||
return formatInt64(int64(i))
|
||||
}
|
||||
|
||||
func formatUint32(i uint32) template.HTML {
|
||||
return formatInt64(int64(i))
|
||||
}
|
||||
|
||||
func appendSeparatedNumberSpans(rv *strings.Builder, s, separatorClass string) {
|
||||
if len(s) > 0 && s[0] == '-' {
|
||||
s = s[1:]
|
||||
rv.WriteByte('-')
|
||||
func formatSecondaryAmount(a float64, td *TemplateData) string {
|
||||
if td.SecondaryCoin == "BTC" || td.SecondaryCoin == "ETH" {
|
||||
return strconv.FormatFloat(a, 'f', 6, 64)
|
||||
}
|
||||
t := (len(s) - 1) / 3
|
||||
if t <= 0 {
|
||||
rv.WriteString(s)
|
||||
} else {
|
||||
t *= 3
|
||||
rv.WriteString(s[:len(s)-t])
|
||||
for i := len(s) - t; i < len(s); i += 3 {
|
||||
rv.WriteString(`<span class="`)
|
||||
rv.WriteString(separatorClass)
|
||||
rv.WriteString(`">`)
|
||||
rv.WriteString(s[i : i+3])
|
||||
rv.WriteString("</span>")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func appendLeftSeparatedNumberSpans(rv *strings.Builder, s, separatorClass string) {
|
||||
l := len(s)
|
||||
if l <= 3 {
|
||||
rv.WriteString(s)
|
||||
} else {
|
||||
rv.WriteString(s[:3])
|
||||
for i := 3; i < len(s); i += 3 {
|
||||
rv.WriteString(`<span class="`)
|
||||
rv.WriteString(separatorClass)
|
||||
rv.WriteString(`">`)
|
||||
e := i + 3
|
||||
if e > l {
|
||||
e = l
|
||||
}
|
||||
rv.WriteString(s[i:e])
|
||||
rv.WriteString("</span>")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func formatInt64(i int64) template.HTML {
|
||||
s := strconv.FormatInt(i, 10)
|
||||
var rv strings.Builder
|
||||
appendSeparatedNumberSpans(&rv, s, "ns")
|
||||
return template.HTML(rv.String())
|
||||
}
|
||||
|
||||
func formatBigInt(i *big.Int) template.HTML {
|
||||
if i == nil {
|
||||
return ""
|
||||
}
|
||||
s := i.String()
|
||||
var rv strings.Builder
|
||||
appendSeparatedNumberSpans(&rv, s, "ns")
|
||||
return template.HTML(rv.String())
|
||||
return strconv.FormatFloat(a, 'f', 2, 64)
|
||||
}
|
||||
|
||||
func getAddressAlias(a string, td *TemplateData) *api.AddressAlias {
|
||||
|
||||
@ -4,13 +4,11 @@ package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"html/template"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
@ -1609,165 +1607,3 @@ func Test_PublicServer_BitcoinType_ExtendedIndex(t *testing.T) {
|
||||
|
||||
httpTestsExtendedIndex(t, ts)
|
||||
}
|
||||
|
||||
func Test_formatInt64(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
n int64
|
||||
want template.HTML
|
||||
}{
|
||||
{"1", 1, "1"},
|
||||
{"13", 13, "13"},
|
||||
{"123", 123, "123"},
|
||||
{"1234", 1234, `1<span class="ns">234</span>`},
|
||||
{"91234", 91234, `91<span class="ns">234</span>`},
|
||||
{"891234", 891234, `891<span class="ns">234</span>`},
|
||||
{"7891234", 7891234, `7<span class="ns">891</span><span class="ns">234</span>`},
|
||||
{"67891234", 67891234, `67<span class="ns">891</span><span class="ns">234</span>`},
|
||||
{"567891234", 567891234, `567<span class="ns">891</span><span class="ns">234</span>`},
|
||||
{"4567891234", 4567891234, `4<span class="ns">567</span><span class="ns">891</span><span class="ns">234</span>`},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := formatInt64(tt.n); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("formatInt64() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_formatTime(t *testing.T) {
|
||||
timeNow = fixedTimeNow
|
||||
tests := []struct {
|
||||
name string
|
||||
want template.HTML
|
||||
}{
|
||||
{
|
||||
name: "2020-12-23 15:16:17",
|
||||
want: `<span tt="2020-12-23 15:16:17">630 days 21 hours ago</span>`,
|
||||
},
|
||||
{
|
||||
name: "2022-08-23 11:12:13",
|
||||
want: `<span tt="2022-08-23 11:12:13">23 days 1 hour ago</span>`,
|
||||
},
|
||||
{
|
||||
name: "2022-09-14 11:12:13",
|
||||
want: `<span tt="2022-09-14 11:12:13">1 day 1 hour ago</span>`,
|
||||
},
|
||||
{
|
||||
name: "2022-09-14 14:12:13",
|
||||
want: `<span tt="2022-09-14 14:12:13">22 hours 31 mins ago</span>`,
|
||||
},
|
||||
{
|
||||
name: "2022-09-15 09:33:26",
|
||||
want: `<span tt="2022-09-15 09:33:26">3 hours 10 mins ago</span>`,
|
||||
},
|
||||
{
|
||||
name: "2022-09-15 12:23:56",
|
||||
want: `<span tt="2022-09-15 12:23:56">20 mins ago</span>`,
|
||||
},
|
||||
{
|
||||
name: "2022-09-15 12:24:07",
|
||||
want: `<span tt="2022-09-15 12:24:07">19 mins ago</span>`,
|
||||
},
|
||||
{
|
||||
name: "2022-09-15 12:43:21",
|
||||
want: `<span tt="2022-09-15 12:43:21">35 secs ago</span>`,
|
||||
},
|
||||
{
|
||||
name: "2022-09-15 12:43:56",
|
||||
want: `<span tt="2022-09-15 12:43:56">0 secs ago</span>`,
|
||||
},
|
||||
{
|
||||
name: "2022-09-16 12:43:56",
|
||||
want: `2022-09-16 12:43:56`,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tm, _ := time.Parse("2006-01-02 15:04:05", tt.name)
|
||||
if got := timeSpan(&tm); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("formatTime() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_appendAmountSpan(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
class string
|
||||
amount string
|
||||
shortcut string
|
||||
txDate string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "prim-amt 1.23456789 BTC",
|
||||
class: "prim-amt",
|
||||
amount: "1.23456789",
|
||||
shortcut: "BTC",
|
||||
want: `<span class="prim-amt">1.<span class="amt-dec">234<span class="ns">567</span><span class="ns">89</span></span> BTC</span>`,
|
||||
},
|
||||
{
|
||||
name: "prim-amt 1432134.23456 BTC",
|
||||
class: "prim-amt",
|
||||
amount: "1432134.23456",
|
||||
shortcut: "BTC",
|
||||
want: `<span class="prim-amt">1<span class="nc">432</span><span class="nc">134</span>.<span class="amt-dec">234<span class="ns">56</span></span> BTC</span>`,
|
||||
},
|
||||
{
|
||||
name: "sec-amt 1 EUR",
|
||||
class: "sec-amt",
|
||||
amount: "1",
|
||||
shortcut: "EUR",
|
||||
want: `<span class="sec-amt">1 EUR</span>`,
|
||||
},
|
||||
{
|
||||
name: "sec-amt -1 EUR",
|
||||
class: "sec-amt",
|
||||
amount: "-1",
|
||||
shortcut: "EUR",
|
||||
want: `<span class="sec-amt">-1 EUR</span>`,
|
||||
},
|
||||
{
|
||||
name: "sec-amt 432109.23 EUR",
|
||||
class: "sec-amt",
|
||||
amount: "432109.23",
|
||||
shortcut: "EUR",
|
||||
want: `<span class="sec-amt">432<span class="nc">109</span>.<span class="amt-dec">23</span> EUR</span>`,
|
||||
},
|
||||
{
|
||||
name: "sec-amt -432109.23 EUR",
|
||||
class: "sec-amt",
|
||||
amount: "-432109.23",
|
||||
shortcut: "EUR",
|
||||
want: `<span class="sec-amt">-432<span class="nc">109</span>.<span class="amt-dec">23</span> EUR</span>`,
|
||||
},
|
||||
{
|
||||
name: "sec-amt 43141.29 EUR",
|
||||
class: "sec-amt",
|
||||
amount: "43141.29",
|
||||
shortcut: "EUR",
|
||||
txDate: "2022-03-14",
|
||||
want: `<span class="sec-amt" tm="2022-03-14">43<span class="nc">141</span>.<span class="amt-dec">29</span> EUR</span>`,
|
||||
},
|
||||
{
|
||||
name: "sec-amt -43141.29 EUR",
|
||||
class: "sec-amt",
|
||||
amount: "-43141.29",
|
||||
shortcut: "EUR",
|
||||
txDate: "2022-03-14",
|
||||
want: `<span class="sec-amt" tm="2022-03-14">-43<span class="nc">141</span>.<span class="amt-dec">29</span> EUR</span>`,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var rv strings.Builder
|
||||
appendAmountSpan(&rv, tt.class, tt.amount, tt.shortcut, tt.txDate)
|
||||
if got := rv.String(); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("formatTime() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user