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