| 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 |