OLD | NEW |
| (Empty) |
1 // Copyright 2016 The LUCI Authors. All rights reserved. | |
2 // Use of this source code is governed under the Apache License, Version 2.0 | |
3 // that can be found in the LICENSE file. | |
4 | |
5 package common | |
6 | |
7 import ( | |
8 "bytes" | |
9 "fmt" | |
10 "html/template" | |
11 "net/http" | |
12 "strings" | |
13 "time" | |
14 | |
15 "github.com/luci/luci-go/milo/api/resp" | |
16 ) | |
17 | |
18 // A collection of useful templating functions | |
19 | |
20 // funcMap is what gets fed into the template bundle. | |
21 var funcMap = template.FuncMap{ | |
22 "humanDuration": humanDuration, | |
23 "parseRFC3339": parseRFC3339, | |
24 "linkify": linkify, | |
25 "linkifySet": linkifySet, | |
26 "obfuscateEmail": obfuscateEmail, | |
27 "localTime": localTime, | |
28 "shortHash": shortHash, | |
29 "startswith": strings.HasPrefix, | |
30 "sub": sub, | |
31 "consoleHeader": consoleHeader, | |
32 "pagedURL": pagedURL, | |
33 "formatTime": formatTime, | |
34 "percent": percent, | |
35 } | |
36 | |
37 // localTime returns a <span> element with t in human format | |
38 // that will be converted to local timezone in the browser. | |
39 // Recommended usage: {{ .Date | localTime "N/A" }} | |
40 func localTime(ifZero string, t time.Time) template.HTML { | |
41 if t.IsZero() { | |
42 return template.HTML(template.HTMLEscapeString(ifZero)) | |
43 } | |
44 milliseconds := t.UnixNano() / 1e6 | |
45 return template.HTML(fmt.Sprintf( | |
46 `<span class="local-time" data-timestamp="%d">%s</span>`, | |
47 milliseconds, | |
48 t.Format(time.RFC850))) | |
49 } | |
50 | |
51 func consoleHeader(brs []resp.BuilderRef) template.HTML { | |
52 // First, split things into nice rows and find the max depth. | |
53 cat := make([][]string, len(brs)) | |
54 depth := 0 | |
55 for i, b := range brs { | |
56 cat[i] = b.Category | |
57 if len(cat[i]) > depth { | |
58 depth = len(cat[i]) | |
59 } | |
60 } | |
61 | |
62 result := "" | |
63 for row := 0; row < depth; row++ { | |
64 result += "<tr><th></th>" | |
65 // "" is the first node, " " is an empty node. | |
66 current := "" | |
67 colspan := 0 | |
68 for _, br := range cat { | |
69 colspan++ | |
70 var s string | |
71 if row >= len(br) { | |
72 s = " " | |
73 } else { | |
74 s = br[row] | |
75 } | |
76 if s != current || current == " " { | |
77 if current != "" || current == " " { | |
78 result += fmt.Sprintf(`<th colspan="%d">
%s</th>`, colspan, current) | |
79 colspan = 0 | |
80 } | |
81 current = s | |
82 } | |
83 } | |
84 if colspan != 0 { | |
85 result += fmt.Sprintf(`<th colspan="%d">%s</th>`, colspa
n, current) | |
86 } | |
87 result += "</tr>" | |
88 } | |
89 | |
90 // Last row: The actual builder shortnames. | |
91 result += "<tr><th></th>" | |
92 for _, br := range brs { | |
93 result += fmt.Sprintf("<th>%s</th>", br.ShortName) | |
94 } | |
95 result += "</tr>" | |
96 return template.HTML(result) | |
97 } | |
98 | |
99 // humanDuration translates d into a human readable string of x units y units, | |
100 // where x and y could be in days, hours, minutes, or seconds, whichever is the | |
101 // largest. | |
102 func humanDuration(d time.Duration) string { | |
103 t := int64(d.Seconds()) | |
104 day := t / 86400 | |
105 hr := (t % 86400) / 3600 | |
106 | |
107 if day > 0 { | |
108 if hr != 0 { | |
109 return fmt.Sprintf("%d days %d hrs", day, hr) | |
110 } | |
111 return fmt.Sprintf("%d days", day) | |
112 } | |
113 | |
114 min := (t % 3600) / 60 | |
115 if hr > 0 { | |
116 if min != 0 { | |
117 return fmt.Sprintf("%d hrs %d mins", hr, min) | |
118 } | |
119 return fmt.Sprintf("%d hrs", hr) | |
120 } | |
121 | |
122 sec := t % 60 | |
123 if min > 0 { | |
124 if sec != 0 { | |
125 return fmt.Sprintf("%d mins %d secs", min, sec) | |
126 } | |
127 return fmt.Sprintf("%d mins", min) | |
128 } | |
129 | |
130 if sec != 0 { | |
131 return fmt.Sprintf("%d secs", sec) | |
132 } | |
133 | |
134 if d > time.Millisecond { | |
135 return fmt.Sprintf("%d ms", d/time.Millisecond) | |
136 } | |
137 | |
138 return "0" | |
139 } | |
140 | |
141 // obfuscateEmail converts a string containing email adddress email@address.com | |
142 // into email<junk>@address.com. | |
143 func obfuscateEmail(email string) template.HTML { | |
144 email = template.HTMLEscapeString(email) | |
145 return template.HTML(strings.Replace( | |
146 email, "@", "<span style=\"display:none\">ohnoyoudont</span>@",
-1)) | |
147 } | |
148 | |
149 // parseRFC3339 parses time represented as a RFC3339 or RFC3339Nano string. | |
150 // If cannot parse, returns zero time. | |
151 func parseRFC3339(s string) time.Time { | |
152 t, err := time.Parse(time.RFC3339, s) | |
153 if err == nil { | |
154 return t | |
155 } | |
156 t, err = time.Parse(time.RFC3339Nano, s) | |
157 if err == nil { | |
158 return t | |
159 } | |
160 return time.Time{} | |
161 } | |
162 | |
163 // formatTime takes a time object and returns a formatted RFC3339 string. | |
164 func formatTime(t time.Time) string { | |
165 return t.Format(time.RFC3339) | |
166 } | |
167 | |
168 // linkifyTemplate is the template used in "linkify". Because the template, | |
169 // itself recursively invokes "linkify", we will initialize it in explicitly | |
170 // in "init()". | |
171 // | |
172 // linkifySetTemplate is the template used in "linkifySet". | |
173 var linkifyTemplate, linkifySetTemplate *template.Template | |
174 | |
175 // linkify turns a resp.LinkSet struct into a canonical link. | |
176 func linkify(link *resp.Link) template.HTML { | |
177 if link == nil { | |
178 return "" | |
179 } | |
180 buf := bytes.Buffer{} | |
181 if err := linkifyTemplate.Execute(&buf, link); err != nil { | |
182 panic(err) | |
183 } | |
184 return template.HTML(buf.Bytes()) | |
185 } | |
186 | |
187 // linkifySet turns a resp.Link struct into a canonical link. | |
188 func linkifySet(linkSet resp.LinkSet) template.HTML { | |
189 if len(linkSet) == 0 { | |
190 return "" | |
191 } | |
192 buf := bytes.Buffer{} | |
193 if err := linkifySetTemplate.Execute(&buf, linkSet); err != nil { | |
194 panic(err) | |
195 } | |
196 return template.HTML(buf.Bytes()) | |
197 } | |
198 | |
199 // sub subtracts one number from another, because apparently go templates aren't | |
200 // smart enough to do that. | |
201 func sub(a, b int) int { | |
202 return a - b | |
203 } | |
204 | |
205 // shortHash abbriviates a git hash into 6 characters. | |
206 func shortHash(s string) string { | |
207 if len(s) > 6 { | |
208 return s[0:6] | |
209 } | |
210 return s | |
211 } | |
212 | |
213 // pagedURL returns a self URL with the given cursor and limit paging options. | |
214 // if limit is set to 0, then inherit whatever limit is set in request. If | |
215 // both are unspecified, then limit is omitted. | |
216 func pagedURL(r *http.Request, limit int, cursor string) string { | |
217 if limit == 0 { | |
218 var err error | |
219 limit, err = GetLimit(r) | |
220 if err != nil { | |
221 // This should not happen because the handler should've
already validated the | |
222 // limit earlier in the process. | |
223 panic(err) | |
224 } | |
225 if limit < 0 { | |
226 limit = 0 | |
227 } | |
228 } | |
229 values := r.URL.Query() | |
230 switch cursor { | |
231 case "EMPTY": | |
232 values.Del("cursor") | |
233 case "": | |
234 // Do nothing, just leave the cursor in. | |
235 default: | |
236 values.Set("cursor", cursor) | |
237 } | |
238 switch { | |
239 case limit < 0: | |
240 values.Del("limit") | |
241 case limit > 0: | |
242 values.Set("limit", fmt.Sprintf("%d", limit)) | |
243 } | |
244 result := *r.URL | |
245 result.RawQuery = values.Encode() | |
246 return result.String() | |
247 } | |
248 | |
249 // percent divides one number by a divisor and returns the percentage in string
form. | |
250 func percent(numerator, divisor int) string { | |
251 p := float64(numerator) * 100.0 / float64(divisor) | |
252 return fmt.Sprintf("%.1f", p) | |
253 } | |
254 | |
255 func init() { | |
256 linkifySetTemplate = template.Must( | |
257 template.New("linkifySet"). | |
258 Funcs(template.FuncMap{ | |
259 "linkify": linkify, | |
260 }).Parse( | |
261 `{{ range $i, $link := . }}` + | |
262 `{{ if gt $i 0 }} {{ end }}` + | |
263 `{{ $link | linkify}}` + | |
264 `{{ end }}`)) | |
265 | |
266 linkifyTemplate = template.Must( | |
267 template.New("linkify"). | |
268 Parse( | |
269 `<a href="{{.URL}}">` + | |
270 `{{if .Img}}<img src="{{.Img}}"{{if .Alt
}} alt="{{.Alt}}"{{end}}>` + | |
271 `{{else if .Alias}}[{{.Label}}]` + | |
272 `{{else}}{{.Label}}{{end}}` + | |
273 `</a>`)) | |
274 } | |
OLD | NEW |