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