Commit | Line | Data |
---|---|---|
f35f44b7 AT |
1 | // Copyright (C) 2007, Fredrik Kuivinen <frekui@gmail.com> |
2 | // 2007, Petr Baudis <pasky@suse.cz> | |
3 | // 2008-2011, Jakub Narebski <jnareb@gmail.com> | |
4 | ||
5 | /** | |
6 | * @fileOverview Generic JavaScript code (helper functions) | |
7 | * @license GPLv2 or later | |
8 | */ | |
9 | ||
10 | ||
11 | /* ============================================================ */ | |
12 | /* ............................................................ */ | |
13 | /* Padding */ | |
14 | ||
15 | /** | |
16 | * pad INPUT on the left with STR that is assumed to have visible | |
17 | * width of single character (for example nonbreakable spaces), | |
18 | * to WIDTH characters | |
19 | * | |
20 | * example: padLeftStr(12, 3, '\u00A0') == '\u00A012' | |
21 | * ('\u00A0' is nonbreakable space) | |
22 | * | |
23 | * @param {Number|String} input: number to pad | |
24 | * @param {Number} width: visible width of output | |
25 | * @param {String} str: string to prefix to string, defaults to '\u00A0' | |
26 | * @returns {String} INPUT prefixed with STR x (WIDTH - INPUT.length) | |
27 | */ | |
28 | function padLeftStr(input, width, str) { | |
29 | var prefix = ''; | |
30 | if (typeof str === 'undefined') { | |
31 | ch = '\u00A0'; // using ' ' doesn't work in all browsers | |
32 | } | |
33 | ||
34 | width -= input.toString().length; | |
35 | while (width > 0) { | |
36 | prefix += str; | |
37 | width--; | |
38 | } | |
39 | return prefix + input; | |
40 | } | |
41 | ||
42 | /** | |
43 | * Pad INPUT on the left to WIDTH, using given padding character CH, | |
44 | * for example padLeft('a', 3, '_') is '__a' | |
45 | * padLeft(4, 2) is '04' (same as padLeft(4, 2, '0')) | |
46 | * | |
47 | * @param {String} input: input value converted to string. | |
48 | * @param {Number} width: desired length of output. | |
49 | * @param {String} ch: single character to prefix to string, defaults to '0'. | |
50 | * | |
51 | * @returns {String} Modified string, at least SIZE length. | |
52 | */ | |
53 | function padLeft(input, width, ch) { | |
54 | var s = input + ""; | |
55 | if (typeof ch === 'undefined') { | |
56 | ch = '0'; | |
57 | } | |
58 | ||
59 | while (s.length < width) { | |
60 | s = ch + s; | |
61 | } | |
62 | return s; | |
63 | } | |
64 | ||
65 | ||
66 | /* ............................................................ */ | |
67 | /* Handling browser incompatibilities */ | |
68 | ||
69 | /** | |
70 | * Create XMLHttpRequest object in cross-browser way | |
71 | * @returns XMLHttpRequest object, or null | |
72 | */ | |
73 | function createRequestObject() { | |
74 | try { | |
75 | return new XMLHttpRequest(); | |
76 | } catch (e) {} | |
77 | try { | |
78 | return window.createRequest(); | |
79 | } catch (e) {} | |
80 | try { | |
81 | return new ActiveXObject("Msxml2.XMLHTTP"); | |
82 | } catch (e) {} | |
83 | try { | |
84 | return new ActiveXObject("Microsoft.XMLHTTP"); | |
85 | } catch (e) {} | |
86 | ||
87 | return null; | |
88 | } | |
89 | ||
90 | ||
91 | /** | |
92 | * Insert rule giving specified STYLE to given SELECTOR at the end of | |
93 | * first CSS stylesheet. | |
94 | * | |
95 | * @param {String} selector: CSS selector, e.g. '.class' | |
96 | * @param {String} style: rule contents, e.g. 'background-color: red;' | |
97 | */ | |
98 | function addCssRule(selector, style) { | |
99 | var stylesheet = document.styleSheets[0]; | |
100 | ||
101 | var theRules = []; | |
102 | if (stylesheet.cssRules) { // W3C way | |
103 | theRules = stylesheet.cssRules; | |
104 | } else if (stylesheet.rules) { // IE way | |
105 | theRules = stylesheet.rules; | |
106 | } | |
107 | ||
108 | if (stylesheet.insertRule) { // W3C way | |
109 | stylesheet.insertRule(selector + ' { ' + style + ' }', theRules.length); | |
110 | } else if (stylesheet.addRule) { // IE way | |
111 | stylesheet.addRule(selector, style); | |
112 | } | |
113 | } | |
114 | ||
115 | ||
116 | /* ............................................................ */ | |
117 | /* Support for legacy browsers */ | |
118 | ||
119 | /** | |
120 | * Provides getElementsByClassName method, if there is no native | |
121 | * implementation of this method. | |
122 | * | |
123 | * NOTE that there are limits and differences compared to native | |
124 | * getElementsByClassName as defined by e.g.: | |
125 | * https://developer.mozilla.org/en/DOM/document.getElementsByClassName | |
126 | * http://www.whatwg.org/specs/web-apps/current-work/multipage/dom.html#dom-getelementsbyclassname | |
127 | * http://www.whatwg.org/specs/web-apps/current-work/multipage/dom.html#dom-document-getelementsbyclassname | |
128 | * | |
129 | * Namely, this implementation supports only single class name as | |
130 | * argument and not set of space-separated tokens representing classes, | |
131 | * it returns Array of nodes rather than live NodeList, and has | |
132 | * additional optional argument where you can limit search to given tags | |
133 | * (via getElementsByTagName). | |
134 | * | |
135 | * Based on | |
136 | * http://code.google.com/p/getelementsbyclassname/ | |
137 | * http://www.dustindiaz.com/getelementsbyclass/ | |
138 | * http://stackoverflow.com/questions/1818865/do-we-have-getelementsbyclassname-in-javascript | |
139 | * | |
140 | * See also http://ejohn.org/blog/getelementsbyclassname-speed-comparison/ | |
141 | * | |
142 | * @param {String} class: name of _single_ class to find | |
143 | * @param {String} [taghint] limit search to given tags | |
144 | * @returns {Node[]} array of matching elements | |
145 | */ | |
146 | if (!('getElementsByClassName' in document)) { | |
147 | document.getElementsByClassName = function (classname, taghint) { | |
148 | taghint = taghint || "*"; | |
149 | var elements = (taghint === "*" && document.all) ? | |
150 | document.all : | |
151 | document.getElementsByTagName(taghint); | |
152 | var pattern = new RegExp("(^|\\s)" + classname + "(\\s|$)"); | |
153 | var matches= []; | |
154 | for (var i = 0, j = 0, n = elements.length; i < n; i++) { | |
155 | var el= elements[i]; | |
156 | if (el.className && pattern.test(el.className)) { | |
157 | // matches.push(el); | |
158 | matches[j] = el; | |
159 | j++; | |
160 | } | |
161 | } | |
162 | return matches; | |
163 | }; | |
164 | } // end if | |
165 | ||
166 | ||
167 | /* ............................................................ */ | |
168 | /* unquoting/unescaping filenames */ | |
169 | ||
170 | /**#@+ | |
171 | * @constant | |
172 | */ | |
173 | var escCodeRe = /\\([^0-7]|[0-7]{1,3})/g; | |
174 | var octEscRe = /^[0-7]{1,3}$/; | |
175 | var maybeQuotedRe = /^\"(.*)\"$/; | |
176 | /**#@-*/ | |
177 | ||
178 | /** | |
179 | * unquote maybe C-quoted filename (as used by git, i.e. it is | |
180 | * in double quotes '"' if there is any escape character used) | |
181 | * e.g. 'aa' -> 'aa', '"a\ta"' -> 'a a' | |
182 | * | |
183 | * @param {String} str: git-quoted string | |
184 | * @returns {String} Unquoted and unescaped string | |
185 | * | |
186 | * @globals escCodeRe, octEscRe, maybeQuotedRe | |
187 | */ | |
188 | function unquote(str) { | |
189 | function unq(seq) { | |
190 | var es = { | |
191 | // character escape codes, aka escape sequences (from C) | |
192 | // replacements are to some extent JavaScript specific | |
193 | t: "\t", // tab (HT, TAB) | |
194 | n: "\n", // newline (NL) | |
195 | r: "\r", // return (CR) | |
196 | f: "\f", // form feed (FF) | |
197 | b: "\b", // backspace (BS) | |
198 | a: "\x07", // alarm (bell) (BEL) | |
199 | e: "\x1B", // escape (ESC) | |
200 | v: "\v" // vertical tab (VT) | |
201 | }; | |
202 | ||
203 | if (seq.search(octEscRe) !== -1) { | |
204 | // octal char sequence | |
205 | return String.fromCharCode(parseInt(seq, 8)); | |
206 | } else if (seq in es) { | |
207 | // C escape sequence, aka character escape code | |
208 | return es[seq]; | |
209 | } | |
210 | // quoted ordinary character | |
211 | return seq; | |
212 | } | |
213 | ||
214 | var match = str.match(maybeQuotedRe); | |
215 | if (match) { | |
216 | str = match[1]; | |
217 | // perhaps str = eval('"'+str+'"'); would be enough? | |
218 | str = str.replace(escCodeRe, | |
219 | function (substr, p1, offset, s) { return unq(p1); }); | |
220 | } | |
221 | return str; | |
222 | } | |
223 | ||
224 | /* end of common-lib.js */ | |
225 | // Copyright (C) 2007, Fredrik Kuivinen <frekui@gmail.com> | |
226 | // 2007, Petr Baudis <pasky@suse.cz> | |
227 | // 2008-2011, Jakub Narebski <jnareb@gmail.com> | |
228 | ||
229 | /** | |
230 | * @fileOverview Datetime manipulation: parsing and formatting | |
231 | * @license GPLv2 or later | |
232 | */ | |
233 | ||
234 | ||
235 | /* ............................................................ */ | |
236 | /* parsing and retrieving datetime related information */ | |
237 | ||
238 | /** | |
239 | * used to extract hours and minutes from timezone info, e.g '-0900' | |
240 | * @constant | |
241 | */ | |
242 | var tzRe = /^([+\-])([0-9][0-9])([0-9][0-9])$/; | |
243 | ||
244 | /** | |
245 | * convert numeric timezone +/-ZZZZ to offset from UTC in seconds | |
246 | * | |
247 | * @param {String} timezoneInfo: numeric timezone '(+|-)HHMM' | |
248 | * @returns {Number} offset from UTC in seconds for timezone | |
249 | * | |
250 | * @globals tzRe | |
251 | */ | |
252 | function timezoneOffset(timezoneInfo) { | |
253 | var match = tzRe.exec(timezoneInfo); | |
254 | var tz_sign = (match[1] === '-' ? -1 : +1); | |
255 | var tz_hour = parseInt(match[2],10); | |
256 | var tz_min = parseInt(match[3],10); | |
257 | ||
258 | return tz_sign*(((tz_hour*60) + tz_min)*60); | |
259 | } | |
260 | ||
261 | /** | |
262 | * return local (browser) timezone as offset from UTC in seconds | |
263 | * | |
264 | * @returns {Number} offset from UTC in seconds for local timezone | |
265 | */ | |
266 | function localTimezoneOffset() { | |
267 | // getTimezoneOffset returns the time-zone offset from UTC, | |
268 | // in _minutes_, for the current locale | |
269 | return ((new Date()).getTimezoneOffset() * -60); | |
270 | } | |
271 | ||
272 | /** | |
273 | * return local (browser) timezone as numeric timezone '(+|-)HHMM' | |
274 | * | |
275 | * @returns {String} locat timezone as -/+ZZZZ | |
276 | */ | |
277 | function localTimezoneInfo() { | |
278 | var tzOffsetMinutes = (new Date()).getTimezoneOffset() * -1; | |
279 | ||
280 | return formatTimezoneInfo(0, tzOffsetMinutes); | |
281 | } | |
282 | ||
283 | ||
284 | /** | |
285 | * Parse RFC-2822 date into a Unix timestamp (into epoch) | |
286 | * | |
287 | * @param {String} date: date in RFC-2822 format, e.g. 'Thu, 21 Dec 2000 16:01:07 +0200' | |
288 | * @returns {Number} epoch i.e. seconds since '00:00:00 1970-01-01 UTC' | |
289 | */ | |
290 | function parseRFC2822Date(date) { | |
291 | // Date.parse accepts the IETF standard (RFC 1123 Section 5.2.14 and elsewhere) | |
292 | // date syntax, which is defined in RFC 2822 (obsoletes RFC 822) | |
293 | // and returns number of _milli_seconds since January 1, 1970, 00:00:00 UTC | |
294 | return Date.parse(date) / 1000; | |
295 | } | |
296 | ||
297 | ||
298 | /* ............................................................ */ | |
299 | /* formatting date */ | |
300 | ||
301 | /** | |
302 | * format timezone offset as numerical timezone '(+|-)HHMM' or '(+|-)HH:MM' | |
303 | * | |
304 | * @param {Number} hours: offset in hours, e.g. 2 for '+0200' | |
305 | * @param {Number} [minutes] offset in minutes, e.g. 30 for '-4030'; | |
306 | * it is split into hours if not 0 <= minutes < 60, | |
307 | * for example 1200 would give '+0100'; | |
308 | * defaults to 0 | |
309 | * @param {String} [sep] separator between hours and minutes part, | |
310 | * default is '', might be ':' for W3CDTF (rfc-3339) | |
311 | * @returns {String} timezone in '(+|-)HHMM' or '(+|-)HH:MM' format | |
312 | */ | |
313 | function formatTimezoneInfo(hours, minutes, sep) { | |
314 | minutes = minutes || 0; // to be able to use formatTimezoneInfo(hh) | |
315 | sep = sep || ''; // default format is +/-ZZZZ | |
316 | ||
317 | if (minutes < 0 || minutes > 59) { | |
318 | hours = minutes > 0 ? Math.floor(minutes / 60) : Math.ceil(minutes / 60); | |
319 | minutes = Math.abs(minutes - 60*hours); // sign of minutes is sign of hours | |
320 | // NOTE: this works correctly because there is no UTC-00:30 timezone | |
321 | } | |
322 | ||
323 | var tzSign = hours >= 0 ? '+' : '-'; | |
324 | if (hours < 0) { | |
325 | hours = -hours; // sign is stored in tzSign | |
326 | } | |
327 | ||
328 | return tzSign + padLeft(hours, 2, '0') + sep + padLeft(minutes, 2, '0'); | |
329 | } | |
330 | ||
331 | /** | |
332 | * translate 'utc' and 'local' to numerical timezone | |
333 | * @param {String} timezoneInfo: might be 'utc' or 'local' (browser) | |
334 | */ | |
335 | function normalizeTimezoneInfo(timezoneInfo) { | |
336 | switch (timezoneInfo) { | |
337 | case 'utc': | |
338 | return '+0000'; | |
339 | case 'local': // 'local' is browser timezone | |
340 | return localTimezoneInfo(); | |
341 | } | |
342 | return timezoneInfo; | |
343 | } | |
344 | ||
345 | ||
346 | /** | |
347 | * return date in local time formatted in iso-8601 like format | |
348 | * 'yyyy-mm-dd HH:MM:SS +/-ZZZZ' e.g. '2005-08-07 21:49:46 +0200' | |
349 | * | |
350 | * @param {Number} epoch: seconds since '00:00:00 1970-01-01 UTC' | |
351 | * @param {String} timezoneInfo: numeric timezone '(+|-)HHMM' | |
352 | * @returns {String} date in local time in iso-8601 like format | |
353 | */ | |
354 | function formatDateISOLocal(epoch, timezoneInfo) { | |
355 | // date corrected by timezone | |
356 | var localDate = new Date(1000 * (epoch + | |
357 | timezoneOffset(timezoneInfo))); | |
358 | var localDateStr = // e.g. '2005-08-07' | |
359 | localDate.getUTCFullYear() + '-' + | |
360 | padLeft(localDate.getUTCMonth()+1, 2, '0') + '-' + | |
361 | padLeft(localDate.getUTCDate(), 2, '0'); | |
362 | var localTimeStr = // e.g. '21:49:46' | |
363 | padLeft(localDate.getUTCHours(), 2, '0') + ':' + | |
364 | padLeft(localDate.getUTCMinutes(), 2, '0') + ':' + | |
365 | padLeft(localDate.getUTCSeconds(), 2, '0'); | |
366 | ||
367 | return localDateStr + ' ' + localTimeStr + ' ' + timezoneInfo; | |
368 | } | |
369 | ||
370 | /** | |
371 | * return date in local time formatted in rfc-2822 format | |
372 | * e.g. 'Thu, 21 Dec 2000 16:01:07 +0200' | |
373 | * | |
374 | * @param {Number} epoch: seconds since '00:00:00 1970-01-01 UTC' | |
375 | * @param {String} timezoneInfo: numeric timezone '(+|-)HHMM' | |
376 | * @param {Boolean} [padDay] e.g. 'Sun, 07 Aug' if true, 'Sun, 7 Aug' otherwise | |
377 | * @returns {String} date in local time in rfc-2822 format | |
378 | */ | |
379 | function formatDateRFC2882(epoch, timezoneInfo, padDay) { | |
380 | // A short textual representation of a month, three letters | |
381 | var months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; | |
382 | // A textual representation of a day, three letters | |
383 | var days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; | |
384 | // date corrected by timezone | |
385 | var localDate = new Date(1000 * (epoch + | |
386 | timezoneOffset(timezoneInfo))); | |
387 | var localDateStr = // e.g. 'Sun, 7 Aug 2005' or 'Sun, 07 Aug 2005' | |
388 | days[localDate.getUTCDay()] + ', ' + | |
389 | (padDay ? padLeft(localDate.getUTCDate(),2,'0') : localDate.getUTCDate()) + ' ' + | |
390 | months[localDate.getUTCMonth()] + ' ' + | |
391 | localDate.getUTCFullYear(); | |
392 | var localTimeStr = // e.g. '21:49:46' | |
393 | padLeft(localDate.getUTCHours(), 2, '0') + ':' + | |
394 | padLeft(localDate.getUTCMinutes(), 2, '0') + ':' + | |
395 | padLeft(localDate.getUTCSeconds(), 2, '0'); | |
396 | ||
397 | return localDateStr + ' ' + localTimeStr + ' ' + timezoneInfo; | |
398 | } | |
399 | ||
400 | /* end of datetime.js */ | |
401 | /** | |
402 | * @fileOverview Accessing cookies from JavaScript | |
403 | * @license GPLv2 or later | |
404 | */ | |
405 | ||
406 | /* | |
407 | * Based on subsection "Cookies in JavaScript" of "Professional | |
408 | * JavaScript for Web Developers" by Nicholas C. Zakas and cookie | |
409 | * plugin from jQuery (dual licensed under the MIT and GPL licenses) | |
410 | */ | |
411 | ||
412 | ||
413 | /** | |
414 | * Create a cookie with the given name and value, | |
415 | * and other optional parameters. | |
416 | * | |
417 | * @example | |
418 | * setCookie('foo', 'bar'); // will be deleted when browser exits | |
419 | * setCookie('foo', 'bar', { expires: new Date(Date.parse('Jan 1, 2012')) }); | |
420 | * setCookie('foo', 'bar', { expires: 7 }); // 7 days = 1 week | |
421 | * setCookie('foo', 'bar', { expires: 14, path: '/' }); | |
422 | * | |
423 | * @param {String} sName: Unique name of a cookie (letters, numbers, underscores). | |
424 | * @param {String} sValue: The string value stored in a cookie. | |
425 | * @param {Object} [options] An object literal containing key/value pairs | |
426 | * to provide optional cookie attributes. | |
427 | * @param {String|Number|Date} [options.expires] Either literal string to be used as cookie expires, | |
428 | * or an integer specifying the expiration date from now on in days, | |
429 | * or a Date object to be used as cookie expiration date. | |
430 | * If a negative value is specified or a date in the past), | |
431 | * the cookie will be deleted. | |
432 | * If set to null or omitted, the cookie will be a session cookie | |
433 | * and will not be retained when the browser exits. | |
434 | * @param {String} [options.path] Restrict access of a cookie to particular directory | |
435 | * (default: path of page that created the cookie). | |
436 | * @param {String} [options.domain] Override what web sites are allowed to access cookie | |
437 | * (default: domain of page that created the cookie). | |
438 | * @param {Boolean} [options.secure] If true, the secure attribute of the cookie will be set | |
439 | * and the cookie would be accessible only from secure sites | |
440 | * (cookie transmission will require secure protocol like HTTPS). | |
441 | */ | |
442 | function setCookie(sName, sValue, options) { | |
443 | options = options || {}; | |
444 | if (sValue === null) { | |
445 | sValue = ''; | |
446 | option.expires = 'delete'; | |
447 | } | |
448 | ||
449 | var sCookie = sName + '=' + encodeURIComponent(sValue); | |
450 | ||
451 | if (options.expires) { | |
452 | var oExpires = options.expires, sDate; | |
453 | if (oExpires === 'delete') { | |
454 | sDate = 'Thu, 01 Jan 1970 00:00:00 GMT'; | |
455 | } else if (typeof oExpires === 'string') { | |
456 | sDate = oExpires; | |
457 | } else { | |
458 | var oDate; | |
459 | if (typeof oExpires === 'number') { | |
460 | oDate = new Date(); | |
461 | oDate.setTime(oDate.getTime() + (oExpires * 24 * 60 * 60 * 1000)); // days to ms | |
462 | } else { | |
463 | oDate = oExpires; | |
464 | } | |
465 | sDate = oDate.toGMTString(); | |
466 | } | |
467 | sCookie += '; expires=' + sDate; | |
468 | } | |
469 | ||
470 | if (options.path) { | |
471 | sCookie += '; path=' + (options.path); | |
472 | } | |
473 | if (options.domain) { | |
474 | sCookie += '; domain=' + (options.domain); | |
475 | } | |
476 | if (options.secure) { | |
477 | sCookie += '; secure'; | |
478 | } | |
479 | document.cookie = sCookie; | |
480 | } | |
481 | ||
482 | /** | |
483 | * Get the value of a cookie with the given name. | |
484 | * | |
485 | * @param {String} sName: Unique name of a cookie (letters, numbers, underscores) | |
486 | * @returns {String|null} The string value stored in a cookie | |
487 | */ | |
488 | function getCookie(sName) { | |
489 | var sRE = '(?:; )?' + sName + '=([^;]*);?'; | |
490 | var oRE = new RegExp(sRE); | |
491 | if (oRE.test(document.cookie)) { | |
492 | return decodeURIComponent(RegExp['$1']); | |
493 | } else { | |
494 | return null; | |
495 | } | |
496 | } | |
497 | ||
498 | /** | |
499 | * Delete cookie with given name | |
500 | * | |
501 | * @param {String} sName: Unique name of a cookie (letters, numbers, underscores) | |
502 | * @param {Object} [options] An object literal containing key/value pairs | |
503 | * to provide optional cookie attributes. | |
504 | * @param {String} [options.path] Must be the same as when setting a cookie | |
505 | * @param {String} [options.domain] Must be the same as when setting a cookie | |
506 | */ | |
507 | function deleteCookie(sName, options) { | |
508 | options = options || {}; | |
509 | options.expires = 'delete'; | |
510 | ||
511 | setCookie(sName, '', options); | |
512 | } | |
513 | ||
514 | /* end of cookies.js */ | |
515 | // Copyright (C) 2007, Fredrik Kuivinen <frekui@gmail.com> | |
516 | // 2007, Petr Baudis <pasky@suse.cz> | |
517 | // 2008-2011, Jakub Narebski <jnareb@gmail.com> | |
518 | ||
519 | /** | |
520 | * @fileOverview Detect if JavaScript is enabled, and pass it to server-side | |
521 | * @license GPLv2 or later | |
522 | */ | |
523 | ||
524 | ||
525 | /* ============================================================ */ | |
526 | /* Manipulating links */ | |
527 | ||
528 | /** | |
529 | * used to check if link has 'js' query parameter already (at end), | |
530 | * and other reasons to not add 'js=1' param at the end of link | |
531 | * @constant | |
532 | */ | |
533 | var jsExceptionsRe = /[;?]js=[01](#.*)?$/; | |
534 | ||
535 | /** | |
536 | * Add '?js=1' or ';js=1' to the end of every link in the document | |
537 | * that doesn't have 'js' query parameter set already. | |
538 | * | |
539 | * Links with 'js=1' lead to JavaScript version of given action, if it | |
540 | * exists (currently there is only 'blame_incremental' for 'blame') | |
541 | * | |
542 | * To be used as `window.onload` handler | |
543 | * | |
544 | * @globals jsExceptionsRe | |
545 | */ | |
546 | function fixLinks() { | |
547 | var allLinks = document.getElementsByTagName("a") || document.links; | |
548 | for (var i = 0, len = allLinks.length; i < len; i++) { | |
549 | var link = allLinks[i]; | |
550 | if (!jsExceptionsRe.test(link)) { | |
551 | link.href = link.href.replace(/(#|$)/, | |
552 | (link.href.indexOf('?') === -1 ? '?' : ';') + 'js=1$1'); | |
553 | } | |
554 | } | |
555 | } | |
556 | ||
557 | /* end of javascript-detection.js */ | |
558 | // Copyright (C) 2011, John 'Warthog9' Hawley <warthog9@eaglescrag.net> | |
559 | // 2011, Jakub Narebski <jnareb@gmail.com> | |
560 | ||
561 | /** | |
562 | * @fileOverview Manipulate dates in gitweb output, adjusting timezone | |
563 | * @license GPLv2 or later | |
564 | */ | |
565 | ||
566 | /** | |
567 | * Get common timezone, add UI for changing timezones, and adjust | |
568 | * dates to use requested common timezone. | |
569 | * | |
570 | * This function is called during onload event (added to window.onload). | |
571 | * | |
572 | * @param {String} tzDefault: default timezone, if there is no cookie | |
573 | * @param {Object} tzCookieInfo: object literal with info about cookie to store timezone | |
574 | * @param {String} tzCookieInfo.name: name of cookie to store timezone | |
575 | * @param {String} tzClassName: denotes elements with date to be adjusted | |
576 | */ | |
577 | function onloadTZSetup(tzDefault, tzCookieInfo, tzClassName) { | |
578 | var tzCookieTZ = getCookie(tzCookieInfo.name, tzCookieInfo); | |
579 | var tz = tzDefault; | |
580 | ||
581 | if (tzCookieTZ) { | |
582 | // set timezone to value saved in a cookie | |
583 | tz = tzCookieTZ; | |
584 | // refresh cookie, so its expiration counts from last use of gitweb | |
585 | setCookie(tzCookieInfo.name, tzCookieTZ, tzCookieInfo); | |
586 | } | |
587 | ||
588 | // add UI for changing timezone | |
589 | addChangeTZ(tz, tzCookieInfo, tzClassName); | |
590 | ||
591 | // server-side of gitweb produces datetime in UTC, | |
592 | // so if tz is 'utc' there is no need for changes | |
593 | var nochange = tz === 'utc'; | |
594 | ||
595 | // adjust dates to use specified common timezone | |
596 | fixDatetimeTZ(tz, tzClassName, nochange); | |
597 | } | |
598 | ||
599 | ||
600 | /* ...................................................................... */ | |
601 | /* Changing dates to use requested timezone */ | |
602 | ||
603 | /** | |
604 | * Replace RFC-2822 dates contained in SPAN elements with tzClassName | |
605 | * CSS class with equivalent dates in given timezone. | |
606 | * | |
607 | * @param {String} tz: numeric timezone in '(-|+)HHMM' format, or 'utc', or 'local' | |
608 | * @param {String} tzClassName: specifies elements to be changed | |
609 | * @param {Boolean} nochange: markup for timezone change, but don't change it | |
610 | */ | |
611 | function fixDatetimeTZ(tz, tzClassName, nochange) { | |
612 | // sanity check, method should be ensured by common-lib.js | |
613 | if (!document.getElementsByClassName) { | |
614 | return; | |
615 | } | |
616 | ||
617 | // translate to timezone in '(-|+)HHMM' format | |
618 | tz = normalizeTimezoneInfo(tz); | |
619 | ||
620 | // NOTE: result of getElementsByClassName should probably be cached | |
621 | var classesFound = document.getElementsByClassName(tzClassName, "span"); | |
622 | for (var i = 0, len = classesFound.length; i < len; i++) { | |
623 | var curElement = classesFound[i]; | |
624 | ||
625 | curElement.title = 'Click to change timezone'; | |
626 | if (!nochange) { | |
627 | // we use *.firstChild.data (W3C DOM) instead of *.innerHTML | |
628 | // as the latter doesn't always work everywhere in every browser | |
629 | var epoch = parseRFC2822Date(curElement.firstChild.data); | |
630 | var adjusted = formatDateRFC2882(epoch, tz); | |
631 | ||
632 | curElement.firstChild.data = adjusted; | |
633 | } | |
634 | } | |
635 | } | |
636 | ||
637 | ||
638 | /* ...................................................................... */ | |
639 | /* Adding triggers, generating timezone menu, displaying and hiding */ | |
640 | ||
641 | /** | |
642 | * Adds triggers for UI to change common timezone used for dates in | |
643 | * gitweb output: it marks up and/or creates item to click to invoke | |
644 | * timezone change UI, creates timezone UI fragment to be attached, | |
645 | * and installs appropriate onclick trigger (via event delegation). | |
646 | * | |
647 | * @param {String} tzSelected: pre-selected timezone, | |
648 | * 'utc' or 'local' or '(-|+)HHMM' | |
649 | * @param {Object} tzCookieInfo: object literal with info about cookie to store timezone | |
650 | * @param {String} tzClassName: specifies elements to install trigger | |
651 | */ | |
652 | function addChangeTZ(tzSelected, tzCookieInfo, tzClassName) { | |
653 | // make link to timezone UI discoverable | |
654 | addCssRule('.'+tzClassName + ':hover', | |
655 | 'text-decoration: underline; cursor: help;'); | |
656 | ||
657 | // create form for selecting timezone (to be saved in a cookie) | |
658 | var tzSelectFragment = document.createDocumentFragment(); | |
659 | tzSelectFragment = createChangeTZForm(tzSelectFragment, | |
660 | tzSelected, tzCookieInfo, tzClassName); | |
661 | ||
662 | // event delegation handler for timezone selection UI (clicking on entry) | |
663 | // see http://www.nczonline.net/blog/2009/06/30/event-delegation-in-javascript/ | |
664 | // assumes that there is no existing document.onclick handler | |
665 | document.onclick = function onclickHandler(event) { | |
666 | //IE doesn't pass in the event object | |
667 | event = event || window.event; | |
668 | ||
669 | //IE uses srcElement as the target | |
670 | var target = event.target || event.srcElement; | |
671 | ||
672 | switch (target.className) { | |
673 | case tzClassName: | |
674 | // don't display timezone menu if it is already displayed | |
675 | if (tzSelectFragment.childNodes.length > 0) { | |
676 | displayChangeTZForm(target, tzSelectFragment); | |
677 | } | |
678 | break; | |
679 | } // end switch | |
680 | }; | |
681 | } | |
682 | ||
683 | /** | |
684 | * Create DocumentFragment with UI for changing common timezone in | |
685 | * which dates are shown in. | |
686 | * | |
687 | * @param {DocumentFragment} documentFragment: where attach UI | |
688 | * @param {String} tzSelected: default (pre-selected) timezone | |
689 | * @param {Object} tzCookieInfo: object literal with info about cookie to store timezone | |
690 | * @returns {DocumentFragment} | |
691 | */ | |
692 | function createChangeTZForm(documentFragment, tzSelected, tzCookieInfo, tzClassName) { | |
693 | var div = document.createElement("div"); | |
694 | div.className = 'popup'; | |
695 | ||
696 | /* '<div class="close-button" title="(click on this box to close)">X</div>' */ | |
697 | var closeButton = document.createElement('div'); | |
698 | closeButton.className = 'close-button'; | |
699 | closeButton.title = '(click on this box to close)'; | |
700 | closeButton.appendChild(document.createTextNode('X')); | |
701 | closeButton.onclick = closeTZFormHandler(documentFragment, tzClassName); | |
702 | div.appendChild(closeButton); | |
703 | ||
704 | /* 'Select timezone: <br clear="all">' */ | |
705 | div.appendChild(document.createTextNode('Select timezone: ')); | |
706 | var br = document.createElement('br'); | |
707 | br.clear = 'all'; | |
708 | div.appendChild(br); | |
709 | ||
710 | /* '<select name="tzoffset"> | |
711 | * ... | |
712 | * <option value="-0700">UTC-07:00</option> | |
713 | * <option value="-0600">UTC-06:00</option> | |
714 | * ... | |
715 | * </select>' */ | |
716 | var select = document.createElement("select"); | |
717 | select.name = "tzoffset"; | |
718 | //select.style.clear = 'all'; | |
719 | select.appendChild(generateTZOptions(tzSelected)); | |
720 | select.onchange = selectTZHandler(documentFragment, tzCookieInfo, tzClassName); | |
721 | div.appendChild(select); | |
722 | ||
723 | documentFragment.appendChild(div); | |
724 | ||
725 | return documentFragment; | |
726 | } | |
727 | ||
728 | ||
729 | /** | |
730 | * Hide (remove from DOM) timezone change UI, ensuring that it is not | |
731 | * garbage collected and that it can be re-enabled later. | |
732 | * | |
733 | * @param {DocumentFragment} documentFragment: contains detached UI | |
734 | * @param {HTMLSelectElement} target: select element inside of UI | |
735 | * @param {String} tzClassName: specifies element where UI was installed | |
736 | * @returns {DocumentFragment} documentFragment | |
737 | */ | |
738 | function removeChangeTZForm(documentFragment, target, tzClassName) { | |
739 | // find containing element, where we appended timezone selection UI | |
740 | // `target' is somewhere inside timezone menu | |
741 | var container = target.parentNode, popup = target; | |
742 | while (container && | |
743 | container.className !== tzClassName) { | |
744 | popup = container; | |
745 | container = container.parentNode; | |
746 | } | |
747 | // safety check if we found correct container, | |
748 | // and if it isn't deleted already | |
749 | if (!container || !popup || | |
750 | container.className !== tzClassName || | |
751 | popup.className !== 'popup') { | |
752 | return documentFragment; | |
753 | } | |
754 | ||
755 | // timezone selection UI was appended as last child | |
756 | // see also displayChangeTZForm function | |
757 | var removed = popup.parentNode.removeChild(popup); | |
758 | if (documentFragment.firstChild !== removed) { // the only child | |
759 | // re-append it so it would be available for next time | |
760 | documentFragment.appendChild(removed); | |
761 | } | |
762 | // all of inline style was added by this script | |
763 | // it is not really needed to remove it, but it is a good practice | |
764 | container.removeAttribute('style'); | |
765 | ||
766 | return documentFragment; | |
767 | } | |
768 | ||
769 | ||
770 | /** | |
771 | * Display UI for changing common timezone for dates in gitweb output. | |
772 | * To be used from 'onclick' event handler. | |
773 | * | |
774 | * @param {HTMLElement} target: where to install/display UI | |
775 | * @param {DocumentFragment} tzSelectFragment: timezone selection UI | |
776 | */ | |
777 | function displayChangeTZForm(target, tzSelectFragment) { | |
778 | // for absolute positioning to be related to target element | |
779 | target.style.position = 'relative'; | |
780 | target.style.display = 'inline-block'; | |
781 | ||
782 | // show/display UI for changing timezone | |
783 | target.appendChild(tzSelectFragment); | |
784 | } | |
785 | ||
786 | ||
787 | /* ...................................................................... */ | |
788 | /* List of timezones for timezone selection menu */ | |
789 | ||
790 | /** | |
791 | * Generate list of timezones for creating timezone select UI | |
792 | * | |
793 | * @returns {Object[]} list of e.g. { value: '+0100', descr: 'GMT+01:00' } | |
794 | */ | |
795 | function generateTZList() { | |
796 | var timezones = [ | |
797 | { value: "utc", descr: "UTC/GMT"}, | |
798 | { value: "local", descr: "Local (per browser)"} | |
799 | ]; | |
800 | ||
801 | // generate all full hour timezones (no fractional timezones) | |
802 | for (var x = -12, idx = timezones.length; x <= +14; x++, idx++) { | |
803 | var hours = (x >= 0 ? '+' : '-') + padLeft(x >=0 ? x : -x, 2); | |
804 | timezones[idx] = { value: hours + '00', descr: 'UTC' + hours + ':00'}; | |
805 | if (x === 0) { | |
806 | timezones[idx].descr = 'UTC\u00B100:00'; // 'UTC±00:00' | |
807 | } | |
808 | } | |
809 | ||
810 | return timezones; | |
811 | } | |
812 | ||
813 | /** | |
814 | * Generate <options> elements for timezone select UI | |
815 | * | |
816 | * @param {String} tzSelected: default timezone | |
817 | * @returns {DocumentFragment} list of options elements to appendChild | |
818 | */ | |
819 | function generateTZOptions(tzSelected) { | |
820 | var elems = document.createDocumentFragment(); | |
821 | var timezones = generateTZList(); | |
822 | ||
823 | for (var i = 0, len = timezones.length; i < len; i++) { | |
824 | var tzone = timezones[i]; | |
825 | var option = document.createElement("option"); | |
826 | if (tzone.value === tzSelected) { | |
827 | option.defaultSelected = true; | |
828 | } | |
829 | option.value = tzone.value; | |
830 | option.appendChild(document.createTextNode(tzone.descr)); | |
831 | ||
832 | elems.appendChild(option); | |
833 | } | |
834 | ||
835 | return elems; | |
836 | } | |
837 | ||
838 | ||
839 | /* ...................................................................... */ | |
840 | /* Event handlers and/or their generators */ | |
841 | ||
842 | /** | |
843 | * Create event handler that select timezone and closes timezone select UI. | |
844 | * To be used as $('select[name="tzselect"]').onchange handler. | |
845 | * | |
846 | * @param {DocumentFragment} tzSelectFragment: timezone selection UI | |
847 | * @param {Object} tzCookieInfo: object literal with info about cookie to store timezone | |
848 | * @param {String} tzCookieInfo.name: name of cookie to save result of selection | |
849 | * @param {String} tzClassName: specifies element where UI was installed | |
850 | * @returns {Function} event handler | |
851 | */ | |
852 | function selectTZHandler(tzSelectFragment, tzCookieInfo, tzClassName) { | |
853 | //return function selectTZ(event) { | |
854 | return function (event) { | |
855 | event = event || window.event; | |
856 | var target = event.target || event.srcElement; | |
857 | ||
858 | var selected = target.options.item(target.selectedIndex); | |
859 | removeChangeTZForm(tzSelectFragment, target, tzClassName); | |
860 | ||
861 | if (selected) { | |
862 | selected.defaultSelected = true; | |
863 | setCookie(tzCookieInfo.name, selected.value, tzCookieInfo); | |
864 | fixDatetimeTZ(selected.value, tzClassName); | |
865 | } | |
866 | }; | |
867 | } | |
868 | ||
869 | /** | |
870 | * Create event handler that closes timezone select UI. | |
871 | * To be used e.g. as $('.closebutton').onclick handler. | |
872 | * | |
873 | * @param {DocumentFragment} tzSelectFragment: timezone selection UI | |
874 | * @param {String} tzClassName: specifies element where UI was installed | |
875 | * @returns {Function} event handler | |
876 | */ | |
877 | function closeTZFormHandler(tzSelectFragment, tzClassName) { | |
878 | //return function closeTZForm(event) { | |
879 | return function (event) { | |
880 | event = event || window.event; | |
881 | var target = event.target || event.srcElement; | |
882 | ||
883 | removeChangeTZForm(tzSelectFragment, target, tzClassName); | |
884 | }; | |
885 | } | |
886 | ||
887 | /* end of adjust-timezone.js */ | |
888 | // Copyright (C) 2007, Fredrik Kuivinen <frekui@gmail.com> | |
889 | // 2007, Petr Baudis <pasky@suse.cz> | |
890 | // 2008-2011, Jakub Narebski <jnareb@gmail.com> | |
891 | ||
892 | /** | |
893 | * @fileOverview JavaScript side of Ajax-y 'blame_incremental' view in gitweb | |
894 | * @license GPLv2 or later | |
895 | */ | |
896 | ||
897 | /* ============================================================ */ | |
898 | /* | |
899 | * This code uses DOM methods instead of (nonstandard) innerHTML | |
900 | * to modify page. | |
901 | * | |
902 | * innerHTML is non-standard IE extension, though supported by most | |
903 | * browsers; however Firefox up to version 1.5 didn't implement it in | |
904 | * a strict mode (application/xml+xhtml mimetype). | |
905 | * | |
906 | * Also my simple benchmarks show that using elem.firstChild.data = | |
907 | * 'content' is slightly faster than elem.innerHTML = 'content'. It | |
908 | * is however more fragile (text element fragment must exists), and | |
909 | * less feature-rich (we cannot add HTML). | |
910 | * | |
911 | * Note that DOM 2 HTML is preferred over generic DOM 2 Core; the | |
912 | * equivalent using DOM 2 Core is usually shown in comments. | |
913 | */ | |
914 | ||
915 | ||
916 | /* ............................................................ */ | |
917 | /* utility/helper functions (and variables) */ | |
918 | ||
919 | var projectUrl; // partial query + separator ('?' or ';') | |
920 | ||
921 | // 'commits' is an associative map. It maps SHA1s to Commit objects. | |
922 | var commits = {}; | |
923 | ||
924 | /** | |
925 | * constructor for Commit objects, used in 'blame' | |
926 | * @class Represents a blamed commit | |
927 | * @param {String} sha1: SHA-1 identifier of a commit | |
928 | */ | |
929 | function Commit(sha1) { | |
930 | if (this instanceof Commit) { | |
931 | this.sha1 = sha1; | |
932 | this.nprevious = 0; /* number of 'previous', effective parents */ | |
933 | } else { | |
934 | return new Commit(sha1); | |
935 | } | |
936 | } | |
937 | ||
938 | /* ............................................................ */ | |
939 | /* progress info, timing, error reporting */ | |
940 | ||
941 | var blamedLines = 0; | |
942 | var totalLines = '???'; | |
943 | var div_progress_bar; | |
944 | var div_progress_info; | |
945 | ||
946 | /** | |
947 | * Detects how many lines does a blamed file have, | |
948 | * This information is used in progress info | |
949 | * | |
950 | * @returns {Number|String} Number of lines in file, or string '...' | |
951 | */ | |
952 | function countLines() { | |
953 | var table = | |
954 | document.getElementById('blame_table') || | |
955 | document.getElementsByTagName('table')[0]; | |
956 | ||
957 | if (table) { | |
958 | return table.getElementsByTagName('tr').length - 1; // for header | |
959 | } else { | |
960 | return '...'; | |
961 | } | |
962 | } | |
963 | ||
964 | /** | |
965 | * update progress info and length (width) of progress bar | |
966 | * | |
967 | * @globals div_progress_info, div_progress_bar, blamedLines, totalLines | |
968 | */ | |
969 | function updateProgressInfo() { | |
970 | if (!div_progress_info) { | |
971 | div_progress_info = document.getElementById('progress_info'); | |
972 | } | |
973 | if (!div_progress_bar) { | |
974 | div_progress_bar = document.getElementById('progress_bar'); | |
975 | } | |
976 | if (!div_progress_info && !div_progress_bar) { | |
977 | return; | |
978 | } | |
979 | ||
980 | var percentage = Math.floor(100.0*blamedLines/totalLines); | |
981 | ||
982 | if (div_progress_info) { | |
983 | div_progress_info.firstChild.data = blamedLines + ' / ' + totalLines + | |
984 | ' (' + padLeftStr(percentage, 3, '\u00A0') + '%)'; | |
985 | } | |
986 | ||
987 | if (div_progress_bar) { | |
988 | //div_progress_bar.setAttribute('style', 'width: '+percentage+'%;'); | |
989 | div_progress_bar.style.width = percentage + '%'; | |
990 | } | |
991 | } | |
992 | ||
993 | ||
994 | var t_interval_server = ''; | |
995 | var cmds_server = ''; | |
996 | var t0 = new Date(); | |
997 | ||
998 | /** | |
999 | * write how much it took to generate data, and to run script | |
1000 | * | |
1001 | * @globals t0, t_interval_server, cmds_server | |
1002 | */ | |
1003 | function writeTimeInterval() { | |
1004 | var info_time = document.getElementById('generating_time'); | |
1005 | if (!info_time || !t_interval_server) { | |
1006 | return; | |
1007 | } | |
1008 | var t1 = new Date(); | |
1009 | info_time.firstChild.data += ' + (' + | |
1010 | t_interval_server + ' sec server blame_data / ' + | |
1011 | (t1.getTime() - t0.getTime())/1000 + ' sec client JavaScript)'; | |
1012 | ||
1013 | var info_cmds = document.getElementById('generating_cmd'); | |
1014 | if (!info_time || !cmds_server) { | |
1015 | return; | |
1016 | } | |
1017 | info_cmds.firstChild.data += ' + ' + cmds_server; | |
1018 | } | |
1019 | ||
1020 | /** | |
1021 | * show an error message alert to user within page (in progress info area) | |
1022 | * @param {String} str: plain text error message (no HTML) | |
1023 | * | |
1024 | * @globals div_progress_info | |
1025 | */ | |
1026 | function errorInfo(str) { | |
1027 | if (!div_progress_info) { | |
1028 | div_progress_info = document.getElementById('progress_info'); | |
1029 | } | |
1030 | if (div_progress_info) { | |
1031 | div_progress_info.className = 'error'; | |
1032 | div_progress_info.firstChild.data = str; | |
1033 | } | |
1034 | } | |
1035 | ||
1036 | /* ............................................................ */ | |
1037 | /* coloring rows during blame_data (git blame --incremental) run */ | |
1038 | ||
1039 | /** | |
1040 | * used to extract N from 'colorN', where N is a number, | |
1041 | * @constant | |
1042 | */ | |
1043 | var colorRe = /\bcolor([0-9]*)\b/; | |
1044 | ||
1045 | /** | |
1046 | * return N if <tr class="colorN">, otherwise return null | |
1047 | * (some browsers require CSS class names to begin with letter) | |
1048 | * | |
1049 | * @param {HTMLElement} tr: table row element to check | |
1050 | * @param {String} tr.className: 'class' attribute of tr element | |
1051 | * @returns {Number|null} N if tr.className == 'colorN', otherwise null | |
1052 | * | |
1053 | * @globals colorRe | |
1054 | */ | |
1055 | function getColorNo(tr) { | |
1056 | if (!tr) { | |
1057 | return null; | |
1058 | } | |
1059 | var className = tr.className; | |
1060 | if (className) { | |
1061 | var match = colorRe.exec(className); | |
1062 | if (match) { | |
1063 | return parseInt(match[1], 10); | |
1064 | } | |
1065 | } | |
1066 | return null; | |
1067 | } | |
1068 | ||
1069 | var colorsFreq = [0, 0, 0]; | |
1070 | /** | |
1071 | * return one of given possible colors (currently least used one) | |
1072 | * example: chooseColorNoFrom(2, 3) returns 2 or 3 | |
1073 | * | |
1074 | * @param {Number[]} arguments: one or more numbers | |
1075 | * assumes that 1 <= arguments[i] <= colorsFreq.length | |
1076 | * @returns {Number} Least used color number from arguments | |
1077 | * @globals colorsFreq | |
1078 | */ | |
1079 | function chooseColorNoFrom() { | |
1080 | // choose the color which is least used | |
1081 | var colorNo = arguments[0]; | |
1082 | for (var i = 1; i < arguments.length; i++) { | |
1083 | if (colorsFreq[arguments[i]-1] < colorsFreq[colorNo-1]) { | |
1084 | colorNo = arguments[i]; | |
1085 | } | |
1086 | } | |
1087 | colorsFreq[colorNo-1]++; | |
1088 | return colorNo; | |
1089 | } | |
1090 | ||
1091 | /** | |
1092 | * given two neighbor <tr> elements, find color which would be different | |
1093 | * from color of both of neighbors; used to 3-color blame table | |
1094 | * | |
1095 | * @param {HTMLElement} tr_prev | |
1096 | * @param {HTMLElement} tr_next | |
1097 | * @returns {Number} color number N such that | |
1098 | * colorN != tr_prev.className && colorN != tr_next.className | |
1099 | */ | |
1100 | function findColorNo(tr_prev, tr_next) { | |
1101 | var color_prev = getColorNo(tr_prev); | |
1102 | var color_next = getColorNo(tr_next); | |
1103 | ||
1104 | ||
1105 | // neither of neighbors has color set | |
1106 | // THEN we can use any of 3 possible colors | |
1107 | if (!color_prev && !color_next) { | |
1108 | return chooseColorNoFrom(1,2,3); | |
1109 | } | |
1110 | ||
1111 | // either both neighbors have the same color, | |
1112 | // or only one of neighbors have color set | |
1113 | // THEN we can use any color except given | |
1114 | var color; | |
1115 | if (color_prev === color_next) { | |
1116 | color = color_prev; // = color_next; | |
1117 | } else if (!color_prev) { | |
1118 | color = color_next; | |
1119 | } else if (!color_next) { | |
1120 | color = color_prev; | |
1121 | } | |
1122 | if (color) { | |
1123 | return chooseColorNoFrom((color % 3) + 1, ((color+1) % 3) + 1); | |
1124 | } | |
1125 | ||
1126 | // neighbors have different colors | |
1127 | // THEN there is only one color left | |
1128 | return (3 - ((color_prev + color_next) % 3)); | |
1129 | } | |
1130 | ||
1131 | /* ............................................................ */ | |
1132 | /* coloring rows like 'blame' after 'blame_data' finishes */ | |
1133 | ||
1134 | /** | |
1135 | * returns true if given row element (tr) is first in commit group | |
1136 | * to be used only after 'blame_data' finishes (after processing) | |
1137 | * | |
1138 | * @param {HTMLElement} tr: table row | |
1139 | * @returns {Boolean} true if TR is first in commit group | |
1140 | */ | |
1141 | function isStartOfGroup(tr) { | |
1142 | return tr.firstChild.className === 'sha1'; | |
1143 | } | |
1144 | ||
1145 | /** | |
1146 | * change colors to use zebra coloring (2 colors) instead of 3 colors | |
1147 | * concatenate neighbor commit groups belonging to the same commit | |
1148 | * | |
1149 | * @globals colorRe | |
1150 | */ | |
1151 | function fixColorsAndGroups() { | |
1152 | var colorClasses = ['light', 'dark']; | |
1153 | var linenum = 1; | |
1154 | var tr, prev_group; | |
1155 | var colorClass = 0; | |
1156 | var table = | |
1157 | document.getElementById('blame_table') || | |
1158 | document.getElementsByTagName('table')[0]; | |
1159 | ||
1160 | while ((tr = document.getElementById('l'+linenum))) { | |
1161 | // index origin is 0, which is table header; start from 1 | |
1162 | //while ((tr = table.rows[linenum])) { // <- it is slower | |
1163 | if (isStartOfGroup(tr, linenum, document)) { | |
1164 | if (prev_group && | |
1165 | prev_group.firstChild.firstChild.href === | |
1166 | tr.firstChild.firstChild.href) { | |
1167 | // we have to concatenate groups | |
1168 | var prev_rows = prev_group.firstChild.rowSpan || 1; | |
1169 | var curr_rows = tr.firstChild.rowSpan || 1; | |
1170 | prev_group.firstChild.rowSpan = prev_rows + curr_rows; | |
1171 | //tr.removeChild(tr.firstChild); | |
1172 | tr.deleteCell(0); // DOM2 HTML way | |
1173 | } else { | |
1174 | colorClass = (colorClass + 1) % 2; | |
1175 | prev_group = tr; | |
1176 | } | |
1177 | } | |
1178 | var tr_class = tr.className; | |
1179 | tr.className = tr_class.replace(colorRe, colorClasses[colorClass]); | |
1180 | linenum++; | |
1181 | } | |
1182 | } | |
1183 | ||
1184 | ||
1185 | /* ============================================================ */ | |
1186 | /* main part: parsing response */ | |
1187 | ||
1188 | /** | |
1189 | * Function called for each blame entry, as soon as it finishes. | |
1190 | * It updates page via DOM manipulation, adding sha1 info, etc. | |
1191 | * | |
1192 | * @param {Commit} commit: blamed commit | |
1193 | * @param {Object} group: object representing group of lines, | |
1194 | * which blame the same commit (blame entry) | |
1195 | * | |
1196 | * @globals blamedLines | |
1197 | */ | |
1198 | function handleLine(commit, group) { | |
1199 | /* | |
1200 | This is the structure of the HTML fragment we are working | |
1201 | with: | |
1202 | ||
1203 | <tr id="l123" class=""> | |
1204 | <td class="sha1" title=""><a href=""> </a></td> | |
1205 | <td class="linenr"><a class="linenr" href="">123</a></td> | |
1206 | <td class="pre"># times (my ext3 doesn't).</td> | |
1207 | </tr> | |
1208 | */ | |
1209 | ||
1210 | var resline = group.resline; | |
1211 | ||
1212 | // format date and time string only once per commit | |
1213 | if (!commit.info) { | |
1214 | /* e.g. 'Kay Sievers, 2005-08-07 21:49:46 +0200' */ | |
1215 | commit.info = commit.author + ', ' + | |
1216 | formatDateISOLocal(commit.authorTime, commit.authorTimezone); | |
1217 | } | |
1218 | ||
1219 | // color depends on group of lines, not only on blamed commit | |
1220 | var colorNo = findColorNo( | |
1221 | document.getElementById('l'+(resline-1)), | |
1222 | document.getElementById('l'+(resline+group.numlines)) | |
1223 | ); | |
1224 | ||
1225 | // loop over lines in commit group | |
1226 | for (var i = 0; i < group.numlines; i++, resline++) { | |
1227 | var tr = document.getElementById('l'+resline); | |
1228 | if (!tr) { | |
1229 | break; | |
1230 | } | |
1231 | /* | |
1232 | <tr id="l123" class=""> | |
1233 | <td class="sha1" title=""><a href=""> </a></td> | |
1234 | <td class="linenr"><a class="linenr" href="">123</a></td> | |
1235 | <td class="pre"># times (my ext3 doesn't).</td> | |
1236 | </tr> | |
1237 | */ | |
1238 | var td_sha1 = tr.firstChild; | |
1239 | var a_sha1 = td_sha1.firstChild; | |
1240 | var a_linenr = td_sha1.nextSibling.firstChild; | |
1241 | ||
1242 | /* <tr id="l123" class=""> */ | |
1243 | var tr_class = ''; | |
1244 | if (colorNo !== null) { | |
1245 | tr_class = 'color'+colorNo; | |
1246 | } | |
1247 | if (commit.boundary) { | |
1248 | tr_class += ' boundary'; | |
1249 | } | |
1250 | if (commit.nprevious === 0) { | |
1251 | tr_class += ' no-previous'; | |
1252 | } else if (commit.nprevious > 1) { | |
1253 | tr_class += ' multiple-previous'; | |
1254 | } | |
1255 | tr.className = tr_class; | |
1256 | ||
1257 | /* <td class="sha1" title="?" rowspan="?"><a href="?">?</a></td> */ | |
1258 | if (i === 0) { | |
1259 | td_sha1.title = commit.info; | |
1260 | td_sha1.rowSpan = group.numlines; | |
1261 | ||
1262 | a_sha1.href = projectUrl + 'a=commit;h=' + commit.sha1; | |
1263 | if (a_sha1.firstChild) { | |
1264 | a_sha1.firstChild.data = commit.sha1.substr(0, 8); | |
1265 | } else { | |
1266 | a_sha1.appendChild( | |
1267 | document.createTextNode(commit.sha1.substr(0, 8))); | |
1268 | } | |
1269 | if (group.numlines >= 2) { | |
1270 | var fragment = document.createDocumentFragment(); | |
1271 | var br = document.createElement("br"); | |
1272 | var match = commit.author.match(/\b([A-Z])\B/g); | |
1273 | if (match) { | |
1274 | var text = document.createTextNode( | |
1275 | match.join('')); | |
1276 | } | |
1277 | if (br && text) { | |
1278 | var elem = fragment || td_sha1; | |
1279 | elem.appendChild(br); | |
1280 | elem.appendChild(text); | |
1281 | if (fragment) { | |
1282 | td_sha1.appendChild(fragment); | |
1283 | } | |
1284 | } | |
1285 | } | |
1286 | } else { | |
1287 | //tr.removeChild(td_sha1); // DOM2 Core way | |
1288 | tr.deleteCell(0); // DOM2 HTML way | |
1289 | } | |
1290 | ||
1291 | /* <td class="linenr"><a class="linenr" href="?">123</a></td> */ | |
1292 | var linenr_commit = | |
1293 | ('previous' in commit ? commit.previous : commit.sha1); | |
1294 | var linenr_filename = | |
1295 | ('file_parent' in commit ? commit.file_parent : commit.filename); | |
1296 | a_linenr.href = projectUrl + 'a=blame_incremental' + | |
1297 | ';hb=' + linenr_commit + | |
1298 | ';f=' + encodeURIComponent(linenr_filename) + | |
1299 | '#l' + (group.srcline + i); | |
1300 | ||
1301 | blamedLines++; | |
1302 | ||
1303 | //updateProgressInfo(); | |
1304 | } | |
1305 | } | |
1306 | ||
1307 | // ---------------------------------------------------------------------- | |
1308 | ||
1309 | /**#@+ | |
1310 | * @constant | |
1311 | */ | |
1312 | var sha1Re = /^([0-9a-f]{40}) ([0-9]+) ([0-9]+) ([0-9]+)/; | |
1313 | var infoRe = /^([a-z-]+) ?(.*)/; | |
1314 | var endRe = /^END ?([^ ]*) ?(.*)/; | |
1315 | /**@-*/ | |
1316 | ||
1317 | var curCommit = new Commit(); | |
1318 | var curGroup = {}; | |
1319 | ||
1320 | /** | |
1321 | * Parse output from 'git blame --incremental [...]', received via | |
1322 | * XMLHttpRequest from server (blamedataUrl), and call handleLine | |
1323 | * (which updates page) as soon as blame entry is completed. | |
1324 | * | |
1325 | * @param {String[]} lines: new complete lines from blamedata server | |
1326 | * | |
1327 | * @globals commits, curCommit, curGroup, t_interval_server, cmds_server | |
1328 | * @globals sha1Re, infoRe, endRe | |
1329 | */ | |
1330 | function processBlameLines(lines) { | |
1331 | var match; | |
1332 | ||
1333 | for (var i = 0, len = lines.length; i < len; i++) { | |
1334 | ||
1335 | if ((match = sha1Re.exec(lines[i]))) { | |
1336 | var sha1 = match[1]; | |
1337 | var srcline = parseInt(match[2], 10); | |
1338 | var resline = parseInt(match[3], 10); | |
1339 | var numlines = parseInt(match[4], 10); | |
1340 | ||
1341 | var c = commits[sha1]; | |
1342 | if (!c) { | |
1343 | c = new Commit(sha1); | |
1344 | commits[sha1] = c; | |
1345 | } | |
1346 | curCommit = c; | |
1347 | ||
1348 | curGroup.srcline = srcline; | |
1349 | curGroup.resline = resline; | |
1350 | curGroup.numlines = numlines; | |
1351 | ||
1352 | } else if ((match = infoRe.exec(lines[i]))) { | |
1353 | var info = match[1]; | |
1354 | var data = match[2]; | |
1355 | switch (info) { | |
1356 | case 'filename': | |
1357 | curCommit.filename = unquote(data); | |
1358 | // 'filename' information terminates the entry | |
1359 | handleLine(curCommit, curGroup); | |
1360 | updateProgressInfo(); | |
1361 | break; | |
1362 | case 'author': | |
1363 | curCommit.author = data; | |
1364 | break; | |
1365 | case 'author-time': | |
1366 | curCommit.authorTime = parseInt(data, 10); | |
1367 | break; | |
1368 | case 'author-tz': | |
1369 | curCommit.authorTimezone = data; | |
1370 | break; | |
1371 | case 'previous': | |
1372 | curCommit.nprevious++; | |
1373 | // store only first 'previous' header | |
1374 | if (!'previous' in curCommit) { | |
1375 | var parts = data.split(' ', 2); | |
1376 | curCommit.previous = parts[0]; | |
1377 | curCommit.file_parent = unquote(parts[1]); | |
1378 | } | |
1379 | break; | |
1380 | case 'boundary': | |
1381 | curCommit.boundary = true; | |
1382 | break; | |
1383 | } // end switch | |
1384 | ||
1385 | } else if ((match = endRe.exec(lines[i]))) { | |
1386 | t_interval_server = match[1]; | |
1387 | cmds_server = match[2]; | |
1388 | ||
1389 | } else if (lines[i] !== '') { | |
1390 | // malformed line | |
1391 | ||
1392 | } // end if (match) | |
1393 | ||
1394 | } // end for (lines) | |
1395 | } | |
1396 | ||
1397 | /** | |
1398 | * Process new data and return pointer to end of processed part | |
1399 | * | |
1400 | * @param {String} unprocessed: new data (from nextReadPos) | |
1401 | * @param {Number} nextReadPos: end of last processed data | |
1402 | * @return {Number} end of processed data (new value for nextReadPos) | |
1403 | */ | |
1404 | function processData(unprocessed, nextReadPos) { | |
1405 | var lastLineEnd = unprocessed.lastIndexOf('\n'); | |
1406 | if (lastLineEnd !== -1) { | |
1407 | var lines = unprocessed.substring(0, lastLineEnd).split('\n'); | |
1408 | nextReadPos += lastLineEnd + 1 /* 1 == '\n'.length */; | |
1409 | ||
1410 | processBlameLines(lines); | |
1411 | } // end if | |
1412 | ||
1413 | return nextReadPos; | |
1414 | } | |
1415 | ||
1416 | /** | |
1417 | * Handle XMLHttpRequest errors | |
1418 | * | |
1419 | * @param {XMLHttpRequest} xhr: XMLHttpRequest object | |
1420 | * @param {Number} [xhr.pollTimer] ID of the timeout to clear | |
1421 | * | |
1422 | * @globals commits | |
1423 | */ | |
1424 | function handleError(xhr) { | |
1425 | errorInfo('Server error: ' + | |
1426 | xhr.status + ' - ' + (xhr.statusText || 'Error contacting server')); | |
1427 | ||
1428 | if (typeof xhr.pollTimer === "number") { | |
1429 | clearTimeout(xhr.pollTimer); | |
1430 | delete xhr.pollTimer; | |
1431 | } | |
1432 | commits = {}; // free memory | |
1433 | } | |
1434 | ||
1435 | /** | |
1436 | * Called after XMLHttpRequest finishes (loads) | |
1437 | * | |
1438 | * @param {XMLHttpRequest} xhr: XMLHttpRequest object | |
1439 | * @param {Number} [xhr.pollTimer] ID of the timeout to clear | |
1440 | * | |
1441 | * @globals commits | |
1442 | */ | |
1443 | function responseLoaded(xhr) { | |
1444 | if (typeof xhr.pollTimer === "number") { | |
1445 | clearTimeout(xhr.pollTimer); | |
1446 | delete xhr.pollTimer; | |
1447 | } | |
1448 | ||
1449 | fixColorsAndGroups(); | |
1450 | writeTimeInterval(); | |
1451 | commits = {}; // free memory | |
1452 | } | |
1453 | ||
1454 | /** | |
1455 | * handler for XMLHttpRequest onreadystatechange event | |
1456 | * @see startBlame | |
1457 | * | |
1458 | * @param {XMLHttpRequest} xhr: XMLHttpRequest object | |
1459 | * @param {Number} xhr.prevDataLength: previous value of xhr.responseText.length | |
1460 | * @param {Number} xhr.nextReadPos: start of unread part of xhr.responseText | |
1461 | * @param {Number} [xhr.pollTimer] ID of the timeout (to reset or cancel) | |
1462 | * @param {Boolean} fromTimer: if handler was called from timer | |
1463 | */ | |
1464 | function handleResponse(xhr, fromTimer) { | |
1465 | ||
1466 | /* | |
1467 | * xhr.readyState | |
1468 | * | |
1469 | * Value Constant (W3C) Description | |
1470 | * ------------------------------------------------------------------- | |
1471 | * 0 UNSENT open() has not been called yet. | |
1472 | * 1 OPENED send() has not been called yet. | |
1473 | * 2 HEADERS_RECEIVED send() has been called, and headers | |
1474 | * and status are available. | |
1475 | * 3 LOADING Downloading; responseText holds partial data. | |
1476 | * 4 DONE The operation is complete. | |
1477 | */ | |
1478 | ||
1479 | if (xhr.readyState !== 4 && xhr.readyState !== 3) { | |
1480 | return; | |
1481 | } | |
1482 | ||
1483 | // the server returned error | |
1484 | // try ... catch block is to work around bug in IE8 | |
1485 | try { | |
1486 | if (xhr.readyState === 3 && xhr.status !== 200) { | |
1487 | return; | |
1488 | } | |
1489 | } catch (e) { | |
1490 | return; | |
1491 | } | |
1492 | if (xhr.readyState === 4 && xhr.status !== 200) { | |
1493 | handleError(xhr); | |
1494 | return; | |
1495 | } | |
1496 | ||
1497 | // In konqueror xhr.responseText is sometimes null here... | |
1498 | if (xhr.responseText === null) { | |
1499 | return; | |
1500 | } | |
1501 | ||
1502 | ||
1503 | // extract new whole (complete) lines, and process them | |
1504 | if (xhr.prevDataLength !== xhr.responseText.length) { | |
1505 | xhr.prevDataLength = xhr.responseText.length; | |
1506 | var unprocessed = xhr.responseText.substring(xhr.nextReadPos); | |
1507 | xhr.nextReadPos = processData(unprocessed, xhr.nextReadPos); | |
1508 | } | |
1509 | ||
1510 | // did we finish work? | |
1511 | if (xhr.readyState === 4) { | |
1512 | responseLoaded(xhr); | |
1513 | return; | |
1514 | } | |
1515 | ||
1516 | // if we get from timer, we have to restart it | |
1517 | // otherwise onreadystatechange gives us partial response, timer not needed | |
1518 | if (fromTimer) { | |
1519 | setTimeout(function () { | |
1520 | handleResponse(xhr, true); | |
1521 | }, 1000); | |
1522 | ||
1523 | } else if (typeof xhr.pollTimer === "number") { | |
1524 | clearTimeout(xhr.pollTimer); | |
1525 | delete xhr.pollTimer; | |
1526 | } | |
1527 | } | |
1528 | ||
1529 | // ============================================================ | |
1530 | // ------------------------------------------------------------ | |
1531 | ||
1532 | /** | |
1533 | * Incrementally update line data in blame_incremental view in gitweb. | |
1534 | * | |
1535 | * @param {String} blamedataUrl: URL to server script generating blame data. | |
1536 | * @param {String} bUrl: partial URL to project, used to generate links. | |
1537 | * | |
1538 | * Called from 'blame_incremental' view after loading table with | |
1539 | * file contents, a base for blame view. | |
1540 | * | |
1541 | * @globals t0, projectUrl, div_progress_bar, totalLines | |
1542 | */ | |
1543 | function startBlame(blamedataUrl, bUrl) { | |
1544 | ||
1545 | var xhr = createRequestObject(); | |
1546 | if (!xhr) { | |
1547 | errorInfo('ERROR: XMLHttpRequest not supported'); | |
1548 | return; | |
1549 | } | |
1550 | ||
1551 | t0 = new Date(); | |
1552 | projectUrl = bUrl + (bUrl.indexOf('?') === -1 ? '?' : ';'); | |
1553 | if ((div_progress_bar = document.getElementById('progress_bar'))) { | |
1554 | //div_progress_bar.setAttribute('style', 'width: 100%;'); | |
1555 | div_progress_bar.style.cssText = 'width: 100%;'; | |
1556 | } | |
1557 | totalLines = countLines(); | |
1558 | updateProgressInfo(); | |
1559 | ||
1560 | /* add extra properties to xhr object to help processing response */ | |
1561 | xhr.prevDataLength = -1; // used to detect if we have new data | |
1562 | xhr.nextReadPos = 0; // where unread part of response starts | |
1563 | ||
1564 | xhr.onreadystatechange = function () { | |
1565 | handleResponse(xhr, false); | |
1566 | }; | |
1567 | ||
1568 | xhr.open('GET', blamedataUrl); | |
1569 | xhr.setRequestHeader('Accept', 'text/plain'); | |
1570 | xhr.send(null); | |
1571 | ||
1572 | // not all browsers call onreadystatechange event on each server flush | |
1573 | // poll response using timer every second to handle this issue | |
1574 | xhr.pollTimer = setTimeout(function () { | |
1575 | handleResponse(xhr, true); | |
1576 | }, 1000); | |
1577 | } | |
1578 | ||
1579 | /* end of blame_incremental.js */ |