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
|
apiV2
|
||||||
)
|
)
|
||||||
|
|
||||||
// PublicServer is a handle to public http server
|
// PublicServer provides public http server functionality
|
||||||
type PublicServer struct {
|
type PublicServer struct {
|
||||||
|
htmlTemplates[TemplateData]
|
||||||
binding string
|
binding string
|
||||||
certFiles string
|
certFiles string
|
||||||
socketio *SocketIoServer
|
socketio *SocketIoServer
|
||||||
@ -55,10 +56,7 @@ type PublicServer struct {
|
|||||||
api *api.Worker
|
api *api.Worker
|
||||||
explorerURL string
|
explorerURL string
|
||||||
internalExplorer bool
|
internalExplorer bool
|
||||||
metrics *common.Metrics
|
|
||||||
is *common.InternalState
|
is *common.InternalState
|
||||||
templates []*template.Template
|
|
||||||
debug bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewPublicServer creates new public server http interface to blockbook and returns its handle
|
// 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{
|
s := &PublicServer{
|
||||||
|
htmlTemplates: htmlTemplates[TemplateData]{
|
||||||
|
metrics: metrics,
|
||||||
|
debug: debugMode,
|
||||||
|
},
|
||||||
binding: binding,
|
binding: binding,
|
||||||
certFiles: certFiles,
|
certFiles: certFiles,
|
||||||
https: https,
|
https: https,
|
||||||
@ -101,10 +103,12 @@ func NewPublicServer(binding string, certFiles string, db *db.RocksDB, chain bch
|
|||||||
mempool: mempool,
|
mempool: mempool,
|
||||||
explorerURL: explorerURL,
|
explorerURL: explorerURL,
|
||||||
internalExplorer: explorerURL == "",
|
internalExplorer: explorerURL == "",
|
||||||
metrics: metrics,
|
|
||||||
is: is,
|
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()
|
s.templates = s.parseTemplates()
|
||||||
|
|
||||||
// map only basic functions, the rest is enabled by method MapFullPublicInterface
|
// 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
|
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 := s.newTemplateData(r)
|
||||||
td.Error = &api.APIError{Text: text}
|
td.Error = error
|
||||||
return td
|
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 (
|
const (
|
||||||
noTpl = tpl(iota)
|
indexTpl = iota + errorInternalTpl + 1
|
||||||
errorTpl
|
|
||||||
errorInternalTpl
|
|
||||||
indexTpl
|
|
||||||
txTpl
|
txTpl
|
||||||
addressTpl
|
addressTpl
|
||||||
xpubTpl
|
xpubTpl
|
||||||
@ -481,7 +417,7 @@ const (
|
|||||||
mempoolTpl
|
mempoolTpl
|
||||||
nftDetailTpl
|
nftDetailTpl
|
||||||
|
|
||||||
tplCount
|
publicTplCount
|
||||||
)
|
)
|
||||||
|
|
||||||
// TemplateData is used to transfer data to the templates
|
// TemplateData is used to transfer data to the templates
|
||||||
@ -590,7 +526,7 @@ func (s *PublicServer) parseTemplates() []*template.Template {
|
|||||||
return t
|
return t
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
t := make([]*template.Template, tplCount)
|
t := make([]*template.Template, publicTplCount)
|
||||||
t[errorTpl] = createTemplate("./static/templates/error.html", "./static/templates/base.html")
|
t[errorTpl] = createTemplate("./static/templates/error.html", "./static/templates/base.html")
|
||||||
t[errorInternalTpl] = 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")
|
t[indexTpl] = createTemplate("./static/templates/index.html", "./static/templates/base.html")
|
||||||
@ -611,85 +547,12 @@ func (s *PublicServer) parseTemplates() []*template.Template {
|
|||||||
return t
|
return t
|
||||||
}
|
}
|
||||||
|
|
||||||
func relativeTimeUnit(d int64) string {
|
func (s *PublicServer) postHtmlTemplateHandler(data *TemplateData, w http.ResponseWriter, r *http.Request) {
|
||||||
var u string
|
// // if SecondaryCoin is specified, set secondary_coin cookie
|
||||||
if d < 60 {
|
if data != nil && data.SecondaryCoin != "" {
|
||||||
if d == 1 {
|
http.SetCookie(w, &http.Cookie{Name: secondaryCoinCookieName, Value: data.SecondaryCoin + "=" + strconv.FormatBool(data.UseSecondaryCoin), Path: "/"})
|
||||||
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 (s *PublicServer) formatAmount(a *api.Amount) string {
|
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))
|
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 {
|
func (s *PublicServer) amountSpan(a *api.Amount, td *TemplateData, classes string) template.HTML {
|
||||||
primary := s.formatAmount(a)
|
primary := s.formatAmount(a)
|
||||||
var rv strings.Builder
|
var rv strings.Builder
|
||||||
@ -898,70 +704,11 @@ func (s *PublicServer) summaryValuesSpan(baseValue float64, secondaryValue float
|
|||||||
return template.HTML(rv.String())
|
return template.HTML(rv.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func formatInt(i int) template.HTML {
|
func formatSecondaryAmount(a float64, td *TemplateData) string {
|
||||||
return formatInt64(int64(i))
|
if td.SecondaryCoin == "BTC" || td.SecondaryCoin == "ETH" {
|
||||||
}
|
return strconv.FormatFloat(a, 'f', 6, 64)
|
||||||
|
|
||||||
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
|
return strconv.FormatFloat(a, 'f', 2, 64)
|
||||||
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())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAddressAlias(a string, td *TemplateData) *api.AddressAlias {
|
func getAddressAlias(a string, td *TemplateData) *api.AddressAlias {
|
||||||
|
|||||||
@ -4,13 +4,11 @@ package server
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"html/template"
|
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
@ -1609,165 +1607,3 @@ func Test_PublicServer_BitcoinType_ExtendedIndex(t *testing.T) {
|
|||||||
|
|
||||||
httpTestsExtendedIndex(t, ts)
|
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