/**
Kallithea JS Files
**/
'use strict';
if (typeof console == "undefined" || typeof console.log == "undefined"){
console = { log: function() {} }
}
/**
* INJECT .html_escape function into String
* Usage: "unsafe string".html_escape()
*
* This is the Javascript equivalent of kallithea.lib.helpers.html_escape(). It
* will escape HTML characters to prevent XSS or other issues. It should be
* used in all cases where Javascript code is inserting potentially unsafe data
* into the document.
*
* For example:
*
* is changed into:
* <script>confirm("boo")</script>
*
*/
String.prototype.html_escape = function() {
return this
.replace(/&/g,'&')
.replace(//g,'>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
/**
* INJECT .format function into String
* Usage: "My name is {0} {1}".format("Johny","Bravo")
* Return "My name is Johny Bravo"
* Inspired by https://gist.github.com/1049426
*/
String.prototype.format = function() {
function format() {
var str = this;
var len = arguments.length+1;
var safe = undefined;
var arg = undefined;
// For each {0} {1} {n...} replace with the argument in that position. If
// the argument is an object or an array it will be stringified to JSON.
for (var i=0; i < len; arg = arguments[i++]) {
safe = typeof arg === 'object' ? JSON.stringify(arg) : arg;
str = str.replace(RegExp('\\{'+(i-1)+'\\}', 'g'), safe);
}
return str;
}
// Save a reference of what may already exist under the property native.
// Allows for doing something like: if("".format.native) { /* use native */ }
format.native = String.prototype.format;
// Replace the prototype property
return format;
}();
String.prototype.strip = function(char) {
if(char === undefined){
char = '\\s';
}
return this.replace(new RegExp('^'+char+'+|'+char+'+$','g'), '');
}
String.prototype.lstrip = function(char) {
if(char === undefined){
char = '\\s';
}
return this.replace(new RegExp('^'+char+'+'),'');
}
String.prototype.rstrip = function(char) {
if(char === undefined){
char = '\\s';
}
return this.replace(new RegExp(''+char+'+$'),'');
}
/* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf#Polyfill
under MIT license / public domain, see
https://developer.mozilla.org/en-US/docs/MDN/About#Copyrights_and_licenses */
if(!Array.prototype.indexOf) {
Array.prototype.indexOf = function (searchElement, fromIndex) {
if ( this === undefined || this === null ) {
throw new TypeError( '"this" is null or not defined' );
}
var length = this.length >>> 0; // Hack to convert object.length to a UInt32
fromIndex = +fromIndex || 0;
if (Math.abs(fromIndex) === Infinity) {
fromIndex = 0;
}
if (fromIndex < 0) {
fromIndex += length;
if (fromIndex < 0) {
fromIndex = 0;
}
}
for (;fromIndex < length; fromIndex++) {
if (this[fromIndex] === searchElement) {
return fromIndex;
}
}
return -1;
};
}
/* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter#Compatibility
under MIT license / public domain, see
https://developer.mozilla.org/en-US/docs/MDN/About#Copyrights_and_licenses */
if (!Array.prototype.filter)
{
Array.prototype.filter = function(fun /*, thisArg */)
{
if (this === void 0 || this === null)
throw new TypeError();
var t = Object(this);
var len = t.length >>> 0;
if (typeof fun !== "function")
throw new TypeError();
var res = [];
var thisArg = arguments.length >= 2 ? arguments[1] : void 0;
for (var i = 0; i < len; i++)
{
if (i in t)
{
var val = t[i];
// NOTE: Technically this should Object.defineProperty at
// the next index, as push can be affected by
// properties on Object.prototype and Array.prototype.
// But that method's new, and collisions should be
// rare, so use the more-compatible alternative.
if (fun.call(thisArg, val, i, t))
res.push(val);
}
}
return res;
};
}
/**
* A customized version of PyRoutes.JS from https://pypi.python.org/pypi/pyroutes.js/
* which is copyright Stephane Klein and was made available under the BSD License.
*
* Usage pyroutes.url('mark_error_fixed',{"error_id":error_id}) // /mark_error_fixed/
*/
var pyroutes = (function() {
var matchlist = {};
var sprintf = (function() {
function get_type(variable) {
return Object.prototype.toString.call(variable).slice(8, -1).toLowerCase();
}
function str_repeat(input, multiplier) {
for (var output = []; multiplier > 0; output[--multiplier] = input) {/* do nothing */}
return output.join('');
}
var str_format = function() {
if (!str_format.cache.hasOwnProperty(arguments[0])) {
str_format.cache[arguments[0]] = str_format.parse(arguments[0]);
}
return str_format.format.call(null, str_format.cache[arguments[0]], arguments);
};
str_format.format = function(parse_tree, argv) {
var cursor = 1, tree_length = parse_tree.length, node_type = '', arg, output = [], i, k, match, pad, pad_character, pad_length;
for (i = 0; i < tree_length; i++) {
node_type = get_type(parse_tree[i]);
if (node_type === 'string') {
output.push(parse_tree[i]);
}
else if (node_type === 'array') {
match = parse_tree[i]; // convenience purposes only
if (match[2]) { // keyword argument
arg = argv[cursor];
for (k = 0; k < match[2].length; k++) {
if (!arg.hasOwnProperty(match[2][k])) {
throw(sprintf('[sprintf] property "%s" does not exist', match[2][k]));
}
arg = arg[match[2][k]];
}
}
else if (match[1]) { // positional argument (explicit)
arg = argv[match[1]];
}
else { // positional argument (implicit)
arg = argv[cursor++];
}
if (/[^s]/.test(match[8]) && (get_type(arg) != 'number')) {
throw(sprintf('[sprintf] expecting number but found %s', get_type(arg)));
}
switch (match[8]) {
case 'b': arg = arg.toString(2); break;
case 'c': arg = String.fromCharCode(arg); break;
case 'd': arg = parseInt(arg, 10); break;
case 'e': arg = match[7] ? arg.toExponential(match[7]) : arg.toExponential(); break;
case 'f': arg = match[7] ? parseFloat(arg).toFixed(match[7]) : parseFloat(arg); break;
case 'o': arg = arg.toString(8); break;
case 's': arg = ((arg = String(arg)) && match[7] ? arg.substring(0, match[7]) : arg); break;
case 'u': arg = Math.abs(arg); break;
case 'x': arg = arg.toString(16); break;
case 'X': arg = arg.toString(16).toUpperCase(); break;
}
arg = (/[def]/.test(match[8]) && match[3] && arg >= 0 ? '+'+ arg : arg);
pad_character = match[4] ? match[4] == '0' ? '0' : match[4].charAt(1) : ' ';
pad_length = match[6] - String(arg).length;
pad = match[6] ? str_repeat(pad_character, pad_length) : '';
output.push(match[5] ? arg + pad : pad + arg);
}
}
return output.join('');
};
str_format.cache = {};
str_format.parse = function(fmt) {
var _fmt = fmt, match = [], parse_tree = [], arg_names = 0;
while (_fmt) {
if ((match = /^[^\x25]+/.exec(_fmt)) !== null) {
parse_tree.push(match[0]);
}
else if ((match = /^\x25{2}/.exec(_fmt)) !== null) {
parse_tree.push('%');
}
else if ((match = /^\x25(?:([1-9]\d*)\$|\(([^\)]+)\))?(\+)?(0|'[^$])?(-)?(\d+)?(?:\.(\d+))?([b-fosuxX])/.exec(_fmt)) !== null) {
if (match[2]) {
arg_names |= 1;
var field_list = [], replacement_field = match[2], field_match = [];
if ((field_match = /^([a-z_][a-z_\d]*)/i.exec(replacement_field)) !== null) {
field_list.push(field_match[1]);
while ((replacement_field = replacement_field.substring(field_match[0].length)) !== '') {
if ((field_match = /^\.([a-z_][a-z_\d]*)/i.exec(replacement_field)) !== null) {
field_list.push(field_match[1]);
}
else if ((field_match = /^\[(\d+)\]/.exec(replacement_field)) !== null) {
field_list.push(field_match[1]);
}
else {
throw('[sprintf] huh?');
}
}
}
else {
throw('[sprintf] huh?');
}
match[2] = field_list;
}
else {
arg_names |= 2;
}
if (arg_names === 3) {
throw('[sprintf] mixing positional and named placeholders is not (yet) supported');
}
parse_tree.push(match);
}
else {
throw('[sprintf] huh?');
}
_fmt = _fmt.substring(match[0].length);
}
return parse_tree;
};
return str_format;
})();
var vsprintf = function(fmt, argv) {
argv.unshift(fmt);
return sprintf.apply(null, argv);
};
return {
'url': function(route_name, params) {
var result = route_name;
if (typeof(params) != 'object'){
params = {};
}
if (matchlist.hasOwnProperty(route_name)) {
var route = matchlist[route_name];
// param substitution
for(var i=0; i < route[1].length; i++) {
if (!params.hasOwnProperty(route[1][i]))
throw new Error(route[1][i] + ' missing in "' + route_name + '" route generation');
}
result = sprintf(route[0], params);
var ret = [];
//extra params => GET
for(var param in params){
if (route[1].indexOf(param) == -1){
ret.push(encodeURIComponent(param) + "=" + encodeURIComponent(params[param]));
}
}
var _parts = ret.join("&");
if(_parts){
result = result +'?'+ _parts
}
}
return result;
},
'register': function(route_name, route_tmpl, req_params) {
if (typeof(req_params) != 'object') {
req_params = [];
}
var keys = [];
for (var i=0; i < req_params.length; i++) {
keys.push(req_params[i]);
}
matchlist[route_name] = [
unescape(route_tmpl),
keys
]
},
'_routes': function(){
return matchlist;
}
}
})();
/* Invoke all functions in callbacks */
var _run_callbacks = function(callbacks){
if (callbacks !== undefined){
var _l = callbacks.length;
for (var i=0;i<_l;i++){
var func = callbacks[i];
if(typeof(func)=='function'){
try{
func();
}catch (err){};
}
}
}
}
/**
* turns objects into GET query string
*/
var _toQueryString = function(o) {
if(typeof o !== 'object') {
return false;
}
var _p, _qs = [];
for(_p in o) {
_qs.push(encodeURIComponent(_p) + '=' + encodeURIComponent(o[_p]));
}
return _qs.join('&');
};
/**
* Load HTML into DOM using Ajax
*
* @param $target: load html async and place it (or an error message) here
* @param success: success callback function
* @param args: query parameters to pass to url
*/
function asynchtml(url, $target, success, args){
if(args===undefined){
args=null;
}
$target.html(_TM['Loading ...']).css('opacity','0.3');
return $.ajax({url: url, data: args, headers: {'X-PARTIAL-XHR': '1'}, cache: false, dataType: 'html'})
.done(function(html) {
$target.html(html);
$target.css('opacity','1.0');
//execute the given original callback
if (success !== undefined && success) {
success();
}
})
.fail(function(jqXHR, textStatus, errorThrown) {
if (textStatus == "abort")
return;
$target.html('ERROR: {0}'.format(textStatus));
$target.css('opacity','1.0');
})
;
};
var ajaxGET = function(url, success, failure) {
if(failure === undefined) {
failure = function(jqXHR, textStatus, errorThrown) {
if (textStatus != "abort")
alert("Ajax GET error: " + textStatus);
};
}
return $.ajax({url: url, headers: {'X-PARTIAL-XHR': '1'}, cache: false})
.done(success)
.fail(failure);
};
var ajaxPOST = function(url, postData, success, failure) {
postData['_session_csrf_secret_token'] = _session_csrf_secret_token;
var postData = _toQueryString(postData);
if(failure === undefined) {
failure = function(jqXHR, textStatus, errorThrown) {
if (textStatus != "abort")
alert("Error posting to server: " + textStatus);
};
}
return $.ajax({url: url, data: postData, type: 'POST', headers: {'X-PARTIAL-XHR': '1'}, cache: false})
.done(success)
.fail(failure);
};
/**
* activate .show_more links
* the .show_more must have an id that is the the id of an element to hide prefixed with _
* the parentnode will be displayed
*/
var show_more_event = function(){
$('.show_more').click(function(e){
var el = e.currentTarget;
$('#' + el.id.substring(1)).hide();
$(el.parentNode).show();
});
};
var _onSuccessFollow = function(target){
var $target = $(target);
var $f_cnt = $('#current_followers_count');
if ($target.hasClass('follow')) {
$target.removeClass('follow').addClass('following');
$target.prop('title', _TM['Stop following this repository']);
if ($f_cnt.html()) {
var cnt = Number($f_cnt.html())+1;
$f_cnt.html(cnt);
}
} else {
$target.removeClass('following').addClass('follow');
$target.prop('title', _TM['Start following this repository']);
if ($f_cnt.html()) {
var cnt = Number($f_cnt.html())-1;
$f_cnt.html(cnt);
}
}
}
var toggleFollowingRepo = function(target, follows_repository_id){
var args = {
'follows_repository_id': follows_repository_id,
'_session_csrf_secret_token': _session_csrf_secret_token
}
$.post(TOGGLE_FOLLOW_URL, args, function(data){
_onSuccessFollow(target);
});
return false;
};
var showRepoSize = function(target, repo_name){
var args = '_session_csrf_secret_token=' + _session_csrf_secret_token;
if(!$("#" + target).hasClass('loaded')){
$("#" + target).html(_TM['Loading ...']);
var url = pyroutes.url('repo_size', {"repo_name":repo_name});
$.post(url, args, function(data) {
$("#" + target).html(data);
$("#" + target).addClass('loaded');
});
}
return false;
};
/**
* load tooltips dynamically based on data attributes, used for .lazy-cs changeset links
*/
var get_changeset_tooltip = function() {
var $target = $(this);
var tooltip = $target.data('tooltip');
if (!tooltip) {
var raw_id = $target.data('raw_id');
var repo_name = $target.data('repo_name');
var url = pyroutes.url('changeset_info', {"repo_name": repo_name, "revision": raw_id});
$.ajax(url, {
async: false,
success: function(data) {
tooltip = data["message"];
}
});
$target.data('tooltip', tooltip);
}
return tooltip;
};
/**
* activate tooltips and popups
*/
var tooltip_activate = function(){
function placement(p, e){
if(e.getBoundingClientRect().top > 2*$(window).height()/3){
return 'top';
}else{
return 'bottom';
}
}
$(document).ready(function(){
$('[data-toggle="tooltip"]').tooltip({
container: 'body',
placement: placement
});
$('[data-toggle="popover"]').popover({
html: true,
container: 'body',
placement: placement,
trigger: 'hover',
template: '
'
});
$('.lazy-cs').tooltip({
title: get_changeset_tooltip,
placement: placement
});
});
};
/**
* Quick filter widget
*
* @param target: filter input target
* @param nodes: list of nodes in html we want to filter.
* @param display_element function that takes current node from nodes and
* does hide or show based on the node
*/
var q_filter = (function() {
var _namespace = {};
var namespace = function (target) {
if (!(target in _namespace)) {
_namespace[target] = {};
}
return _namespace[target];
};
return function (target, $nodes, display_element) {
var $nodes = $nodes;
var $q_filter_field = $('#' + target);
var F = namespace(target);
$q_filter_field.keyup(function (e) {
clearTimeout(F.filterTimeout);
F.filterTimeout = setTimeout(F.updateFilter, 600);
});
F.filterTimeout = null;
F.updateFilter = function () {
// Reset timeout
F.filterTimeout = null;
var obsolete = [];
var req = $q_filter_field.val().toLowerCase();
var showing = 0;
$nodes.each(function () {
var n = this;
var target_element = display_element(n);
if (req && n.innerHTML.toLowerCase().indexOf(req) == -1) {
$(target_element).hide();
}
else {
$(target_element).show();
showing += 1;
}
});
$('#repo_count').html(showing);
/* FIXME: don't hardcode */
}
}
})();
/**
* Comment handling
*/
// move comments to their right location, inside new trs
function move_comments($anchorcomments) {
$anchorcomments.each(function(i, anchorcomment) {
var $anchorcomment = $(anchorcomment);
var target_id = $anchorcomment.data('target-id');
var $comment_div = _get_add_comment_div(target_id);
var f_path = $anchorcomment.data('f_path');
var line_no = $anchorcomment.data('line_no');
if ($comment_div[0]) {
$comment_div.append($anchorcomment.children());
if (f_path && line_no) {
_comment_div_append_add($comment_div, f_path, line_no);
} else {
_comment_div_append_form($comment_div, f_path, line_no);
}
} else {
$anchorcomment.before("Comment to {0} line {1} which is outside the diff context:".format(f_path || '?', line_no || '?'));
}
});
linkInlineComments($('.firstlink'), $('.comment:first-child'));
}
// comment bubble was clicked - insert new tr and show form
function show_comment_form($bubble) {
var children = $bubble.closest('tr.line').children('[id]');
var line_td_id = children[children.length - 1].id;
var $comment_div = _get_add_comment_div(line_td_id);
var f_path = $bubble.closest('[data-f_path]').data('f_path');
var parts = line_td_id.split('_');
var line_no = parts[parts.length-1];
comment_div_state($comment_div, f_path, line_no, true);
}
// return comment div for target_id - add it if it doesn't exist yet
function _get_add_comment_div(target_id) {
var comments_box_id = 'comments-' + target_id;
var $comments_box = $('#' + comments_box_id);
if (!$comments_box.length) {
var html = '
'.format(comments_box_id);
$('#' + target_id).closest('tr').after(html);
$comments_box = $('#' + comments_box_id);
}
return $comments_box;
}
// Set $comment_div state - showing or not showing form and Add button.
// An Add button is shown on non-empty forms when no form is shown.
// The form is controlled by show_form_opt - if undefined, form is only shown for general comments.
function comment_div_state($comment_div, f_path, line_no, show_form_opt) {
var show_form = show_form_opt !== undefined ? show_form_opt : !f_path && !line_no;
var $forms = $comment_div.children('.comment-inline-form');
var $buttonrow = $comment_div.children('.add-button-row');
var $comments = $comment_div.children('.comment:not(.submitting)');
$forms.remove();
$buttonrow.remove();
if (show_form) {
_comment_div_append_form($comment_div, f_path, line_no);
} else if ($comments.length) {
_comment_div_append_add($comment_div, f_path, line_no);
} else {
$comment_div.parent('tr').remove();
}
}
// append an Add button to $comment_div and hook it up to show form
function _comment_div_append_add($comment_div, f_path, line_no) {
var addlabel = TRANSLATION_MAP['Add Another Comment'];
var $add = $('
{0}
'.format(addlabel));
$comment_div.append($add);
$add.children('.add-button').click(function(e) {
comment_div_state($comment_div, f_path, line_no, true);
});
}
// append a comment form to $comment_div
function _comment_div_append_form($comment_div, f_path, line_no) {
var $form_div = $('#comment-inline-form-template').children()
.clone()
.addClass('comment-inline-form');
$comment_div.append($form_div);
var $preview = $comment_div.find("div.comment-preview");
var $form = $comment_div.find("form");
var $textarea = $form.find('textarea');
$form.submit(function(e) {
e.preventDefault();
var text = $textarea.val();
var review_status = $form.find('input:radio[name=changeset_status]:checked').val();
var pr_close = $form.find('input:checkbox[name=save_close]:checked').length ? 'on' : '';
var pr_delete = $form.find('input:checkbox[name=save_delete]:checked').length ? 'delete' : '';
if (!text && !review_status && !pr_close && !pr_delete) {
alert("Please provide a comment");
return false;
}
if (pr_delete) {
if (text || review_status || pr_close) {
alert('Cannot delete pull request while making other changes');
return false;
}
if (!confirm('Confirm to delete this pull request')) {
return false;
}
var comments = $('.comment').length;
if (comments > 0 &&
!confirm('Confirm again to delete this pull request with {0} comments'.format(comments))) {
return false;
}
}
if (review_status) {
var $review_status = $preview.find('.automatic-comment');
var review_status_lbl = $("#comment-inline-form-template input.status_change_radio[value='" + review_status + "']").parent().text().strip();
$review_status.find('.comment-status-label').text(review_status_lbl);
$review_status.show();
}
$preview.find('.comment-text div').text(text);
$preview.show();
$textarea.val('');
if (f_path && line_no) {
$form.hide();
}
var postData = {
'text': text,
'f_path': f_path,
'line': line_no,
'changeset_status': review_status,
'save_close': pr_close,
'save_delete': pr_delete
};
var success = function(json_data) {
if (pr_delete) {
location = json_data['location'];
} else {
$comment_div.append(json_data['rendered_text']);
comment_div_state($comment_div, f_path, line_no);
linkInlineComments($('.firstlink'), $('.comment:first-child'));
if ((review_status || pr_close) && !f_path && !line_no) {
// Page changed a lot - reload it after closing the submitted form
comment_div_state($comment_div, f_path, line_no, false);
location.reload(true);
}
}
};
var failure = function(x, s, e) {
$preview.removeClass('submitting').addClass('failed');
var $status = $preview.find('.comment-submission-status');
$('', {
'title': e,
text: _TM['Unable to post']
}).replaceAll($status.contents());
$('