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