Jump to content

User:Ebrahames/All-in-one.js

From Wikipedia, the free encyclopedia
Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
 /*
 This is an experimental attempt to integrate various user scripts,
 which the user can switch on or off in the preferences page.
 
 Read more at http://wiki.riteme.site/wiki/User:Cameltrader/All-in-one.js/Description
 
 This page was designed with the hope to make scripts more readable.
 It is both valid javascript and valid wikimarkup.
 */

<!-- --><div class="NavFrame" style="text-align: left; background-color: #dde;"><div class="NavHead" style="display: inline; padding-right: 4em; background-color: transparent;">Main.js</div><div class="NavContent" style="text-align: left"><pre><nowiki>


$(function () {
    Ajax.Responders.register({
        onException: function (exception) {
            alert("exception=" + exception + "; message=" + exception.message);
        }
    });
    new CameltraderPluginManager();
});

/*
// Probably a future user script
function fixCyrillicAnchors() {
    // Precompute utf-8-to-codepoint conversion table
    var cyr = {};
    for (var ch = 0x0400; ch <= 0x052f; ch++) {
        var chUtf8 = new Number( ((ch << 2) & 0x1f00) + (ch & 0x3f) + 0xc080 ).toString(16);
        var chPercent = "%" + chUtf8.substring(0, 2) + "%" + chUtf8.substring(2);
        cyr[chPercent] = cyr[chPercent.toUpperCase()] = String.fromCharCode(ch);
    }
    for (var i = 0; i < document.links.length; i++) {
        var href = document.links[i].href;
        var re = /%(D[01])%([0-9a-fA-F]{2})/gi;
        while (true) {
            re.lastIndex = 0;
            var m = re.exec(href);
            if (m == null) {
                break;
            }
            href = href.substring(0, m.index) + cyr[m[0]]
                 + href.substring(m.index + m[0].length);
        }
        document.links[i].href = href;
    }
}
*/
//</nowiki></pre></div></div>
<!-- --><div class="NavFrame" style="text-align: left; background-color: #dde;"><div class="NavHead" style="display: inline; padding-right: 4em; background-color: transparent;">prototype.js</div><div class="NavContent" style="text-align: left"><pre><nowiki>
/*  Prototype JavaScript framework, version 1.5.0
 *  (c) 2005-2007 Sam Stephenson
 *
 *  Prototype is freely distributable under the terms of an MIT-style license.
 *  For details, see the Prototype web site: http://prototype.conio.net/
 *
/*--------------------------------------------------------------------------*/

var Prototype = {
  Version: '1.5.0',
  BrowserFeatures: {
    XPath: !!document.evaluate
  },

  ScriptFragment: '(?:<script.*?>)((\n|\r|.)*?)(?:<\/script>)',
  emptyFunction: function() {},
  K: function(x) { return x }
}

var Class = {
  create: function() {
    return function() {
      this.initialize.apply(this, arguments);
    }
  }
}

var Abstract = new Object();

Object.extend = function(destination, source) {
  for (var property in source) {
    destination[property] = source[property];
  }
  return destination;
}

Object.extend(Object, {
  inspect: function(object) {
    try {
      if (object === undefined) return 'undefined';
      if (object === null) return 'null';
      return object.inspect ? object.inspect() : object.toString();
    } catch (e) {
      if (e instanceof RangeError) return '...';
      throw e;
    }
  },

  keys: function(object) {
    var keys = [];
    for (var property in object)
      keys.push(property);
    return keys;
  },

  values: function(object) {
    var values = [];
    for (var property in object)
      values.push(object[property]);
    return values;
  },

  clone: function(object) {
    return Object.extend({}, object);
  }
});

Function.prototype.bind = function() {
  var __method = this, args = $A(arguments), object = args.shift();
  return function() {
    return __method.apply(object, args.concat($A(arguments)));
  }
}

Function.prototype.bindAsEventListener = function(object) {
  var __method = this, args = $A(arguments), object = args.shift();
  return function(event) {
    return __method.apply(object, [( event || window.event)].concat(args).concat($A(arguments)));
  }
}

Object.extend(Number.prototype, {
  toColorPart: function() {
    var digits = this.toString(16);
    if (this < 16) return '0' + digits;
    return digits;
  },

  succ: function() {
    return this + 1;
  },

  times: function(iterator) {
    $R(0, this, true).each(iterator);
    return this;
  }
});

var Try = {
  these: function() {
    var returnValue;

    for (var i = 0, length = arguments.length; i < length; i++) {
      var lambda = arguments[i];
      try {
        returnValue = lambda();
        break;
      } catch (e) {}
    }

    return returnValue;
  }
}

/*--------------------------------------------------------------------------*/

var PeriodicalExecuter = Class.create();
PeriodicalExecuter.prototype = {
  initialize: function(callback, frequency) {
    this.callback = callback;
    this.frequency = frequency;
    this.currentlyExecuting = false;

    this.registerCallback();
  },

  registerCallback: function() {
    this.timer = setInterval(this.onTimerEvent.bind(this), this.frequency * 1000);
  },

  stop: function() {
    if (!this.timer) return;
    clearInterval(this.timer);
    this.timer = null;
  },

  onTimerEvent: function() {
    if (!this.currentlyExecuting) {
      try {
        this.currentlyExecuting = true;
        this.callback(this);
      } finally {
        this.currentlyExecuting = false;
      }
    }
  }
}
String.interpret = function(value){
  return value == null ? '' : String(value);
}

Object.extend(String.prototype, {
  gsub: function(pattern, replacement) {
    var result = '', source = this, match;
    replacement = arguments.callee.prepareReplacement(replacement);

    while (source.length > 0) {
      if (match = source.match(pattern)) {
        result += source.slice(0, match.index);
        result += String.interpret(replacement(match));
        source  = source.slice(match.index + match[0].length);
      } else {
        result += source, source = '';
      }
    }
    return result;
  },

  sub: function(pattern, replacement, count) {
    replacement = this.gsub.prepareReplacement(replacement);
    count = count === undefined ? 1 : count;

    return this.gsub(pattern, function(match) {
      if (--count < 0) return match[0];
      return replacement(match);
    });
  },

  scan: function(pattern, iterator) {
    this.gsub(pattern, iterator);
    return this;
  },

  truncate: function(length, truncation) {
    length = length || 30;
    truncation = truncation === undefined ? '...' : truncation;
    return this.length > length ?
      this.slice(0, length - truncation.length) + truncation : this;
  },

  strip: function() {
    return this.replace(/^\s+/, '').replace(/\s+$/, '');
  },

  stripTags: function() {
    return this.replace(/<\/?[^>]+>/gi, '');
  },

  stripScripts: function() {
    return this.replace(new RegExp(Prototype.ScriptFragment, 'img'), '');
  },

  extractScripts: function() {
    var matchAll = new RegExp(Prototype.ScriptFragment, 'img');
    var matchOne = new RegExp(Prototype.ScriptFragment, 'im');
    return (this.match(matchAll) || []).map(function(scriptTag) {
      return (scriptTag.match(matchOne) || ['', ''])[1];
    });
  },

  evalScripts: function() {
    return this.extractScripts().map(function(script) { return eval(script) });
  },

  escapeHTML: function() {
    var div = document.createElement('div');
    var text = document.createTextNode(this);
    div.appendChild(text);
    return div.innerHTML;
  },

  unescapeHTML: function() {
    var div = document.createElement('div');
    div.innerHTML = this.stripTags();
    return div.childNodes[0] ? (div.childNodes.length > 1 ?
      $A(div.childNodes).inject('',function(memo,node){ return memo+node.nodeValue }) :
      div.childNodes[0].nodeValue) : '';
  },

  toQueryParams: function(separator) {
    var match = this.strip().match(/([^?#]*)(#.*)?$/);
    if (!match) return {};

    return match[1].split(separator || '&').inject({}, function(hash, pair) {
      if ((pair = pair.split('='))[0]) {
        var name = decodeURIComponent(pair[0]);
        var value = pair[1] ? decodeURIComponent(pair[1]) : undefined;

        if (hash[name] !== undefined) {
          if (hash[name].constructor != Array)
            hash[name] = [hash[name]];
          if (value) hash[name].push(value);
        }
        else hash[name] = value;
      }
      return hash;
    });
  },

  toArray: function() {
    return this.split('');
  },

  succ: function() {
    return this.slice(0, this.length - 1) +
      String.fromCharCode(this.charCodeAt(this.length - 1) + 1);
  },

  camelize: function() {
    var parts = this.split('-'), len = parts.length;
    if (len == 1) return parts[0];

    var camelized = this.charAt(0) == '-'
      ? parts[0].charAt(0).toUpperCase() + parts[0].substring(1)
      : parts[0];

    for (var i = 1; i < len; i++)
      camelized += parts[i].charAt(0).toUpperCase() + parts[i].substring(1);

    return camelized;
  },

  capitalize: function(){
    return this.charAt(0).toUpperCase() + this.substring(1).toLowerCase();
  },

  underscore: function() {
    return this.gsub(/::/, '/').gsub(/([A-Z]+)([A-Z][a-z])/,'#{1}_#{2}').gsub(/([a-z\d])([A-Z])/,'#{1}_#{2}').gsub(/-/,'_').toLowerCase();
  },

  dasherize: function() {
    return this.gsub(/_/,'-');
  },

  inspect: function(useDoubleQuotes) {
    var escapedString = this.replace(/\\/g, '\\\\');
    if (useDoubleQuotes)
      return '"' + escapedString.replace(/"/g, '\\"') + '"';
    else
      return "'" + escapedString.replace(/'/g, '\\\'') + "'";
  }
});

String.prototype.gsub.prepareReplacement = function(replacement) {
  if (typeof replacement == 'function') return replacement;
  var template = new Template(replacement);
  return function(match) { return template.evaluate(match) };
}

String.prototype.parseQuery = String.prototype.toQueryParams;

var Template = Class.create();
Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/;
Template.prototype = {
  initialize: function(template, pattern) {
    this.template = template.toString();
    this.pattern  = pattern || Template.Pattern;
  },

  evaluate: function(object) {
    return this.template.gsub(this.pattern, function(match) {
      var before = match[1];
      if (before == '\\') return match[2];
      return before + String.interpret(object[match[3]]);
    });
  }
}

var $break    = new Object();
var $continue = new Object();

var Enumerable = {
  each: function(iterator) {
    var index = 0;
    try {
      this._each(function(value) {
        try {
          iterator(value, index++);
        } catch (e) {
          if (e != $continue) throw e;
        }
      });
    } catch (e) {
      if (e != $break) throw e;
    }
    return this;
  },

  eachSlice: function(number, iterator) {
    var index = -number, slices = [], array = this.toArray();
    while ((index += number) < array.length)
      slices.push(array.slice(index, index+number));
    return slices.map(iterator);
  },

  all: function(iterator) {
    var result = true;
    this.each(function(value, index) {
      result = result && !!(iterator || Prototype.K)(value, index);
      if (!result) throw $break;
    });
    return result;
  },

  any: function(iterator) {
    var result = false;
    this.each(function(value, index) {
      if (result = !!(iterator || Prototype.K)(value, index))
        throw $break;
    });
    return result;
  },

  collect: function(iterator) {
    var results = [];
    this.each(function(value, index) {
      results.push((iterator || Prototype.K)(value, index));
    });
    return results;
  },

  detect: function(iterator) {
    var result;
    this.each(function(value, index) {
      if (iterator(value, index)) {
        result = value;
        throw $break;
      }
    });
    return result;
  },

  findAll: function(iterator) {
    var results = [];
    this.each(function(value, index) {
      if (iterator(value, index))
        results.push(value);
    });
    return results;
  },

  grep: function(pattern, iterator) {
    var results = [];
    this.each(function(value, index) {
      var stringValue = value.toString();
      if (stringValue.match(pattern))
        results.push((iterator || Prototype.K)(value, index));
    })
    return results;
  },

  include: function(object) {
    var found = false;
    this.each(function(value) {
      if (value == object) {
        found = true;
        throw $break;
      }
    });
    return found;
  },

  inGroupsOf: function(number, fillWith) {
    fillWith = fillWith === undefined ? null : fillWith;
    return this.eachSlice(number, function(slice) {
      while(slice.length < number) slice.push(fillWith);
      return slice;
    });
  },

  inject: function(memo, iterator) {
    this.each(function(value, index) {
      memo = iterator(memo, value, index);
    });
    return memo;
  },

  invoke: function(method) {
    var args = $A(arguments).slice(1);
    return this.map(function(value) {
      return value[method].apply(value, args);
    });
  },

  max: function(iterator) {
    var result;
    this.each(function(value, index) {
      value = (iterator || Prototype.K)(value, index);
      if (result == undefined || value >= result)
        result = value;
    });
    return result;
  },

  min: function(iterator) {
    var result;
    this.each(function(value, index) {
      value = (iterator || Prototype.K)(value, index);
      if (result == undefined || value < result)
        result = value;
    });
    return result;
  },

  partition: function(iterator) {
    var trues = [], falses = [];
    this.each(function(value, index) {
      ((iterator || Prototype.K)(value, index) ?
        trues : falses).push(value);
    });
    return [trues, falses];
  },

  pluck: function(property) {
    var results = [];
    this.each(function(value, index) {
      results.push(value[property]);
    });
    return results;
  },

  reject: function(iterator) {
    var results = [];
    this.each(function(value, index) {
      if (!iterator(value, index))
        results.push(value);
    });
    return results;
  },

  sortBy: function(iterator) {
    return this.map(function(value, index) {
      return {value: value, criteria: iterator(value, index)};
    }).sort(function(left, right) {
      var a = left.criteria, b = right.criteria;
      return a < b ? -1 : a > b ? 1 : 0;
    }).pluck('value');
  },

  toArray: function() {
    return this.map();
  },

  zip: function() {
    var iterator = Prototype.K, args = $A(arguments);
    if (typeof args.last() == 'function')
      iterator = args.pop();

    var collections = [this].concat(args).map($A);
    return this.map(function(value, index) {
      return iterator(collections.pluck(index));
    });
  },

  size: function() {
    return this.toArray().length;
  },

  inspect: function() {
    return '#<Enumerable:' + this.toArray().inspect() + '>';
  }
}

Object.extend(Enumerable, {
  map:     Enumerable.collect,
  find:    Enumerable.detect,
  select:  Enumerable.findAll,
  member:  Enumerable.include,
  entries: Enumerable.toArray
});
var $A = Array.from = function(iterable) {
  if (!iterable) return [];
  if (iterable.toArray) {
    return iterable.toArray();
  } else {
    var results = [];
    for (var i = 0, length = iterable.length; i < length; i++)
      results.push(iterable[i]);
    return results;
  }
}

Object.extend(Array.prototype, Enumerable);

if (!Array.prototype._reverse)
  Array.prototype._reverse = Array.prototype.reverse;

Object.extend(Array.prototype, {
  _each: function(iterator) {
    for (var i = 0, length = this.length; i < length; i++)
      iterator(this[i]);
  },

  clear: function() {
    this.length = 0;
    return this;
  },

  first: function() {
    return this[0];
  },

  last: function() {
    return this[this.length - 1];
  },

  compact: function() {
    return this.select(function(value) {
      return value != null;
    });
  },

  flatten: function() {
    return this.inject([], function(array, value) {
      return array.concat(value && value.constructor == Array ?
        value.flatten() : [value]);
    });
  },

  without: function() {
    var values = $A(arguments);
    return this.select(function(value) {
      return !values.include(value);
    });
  },

  indexOf: function(object) {
    for (var i = 0, length = this.length; i < length; i++)
      if (this[i] == object) return i;
    return -1;
  },

  reverse: function(inline) {
    return (inline !== false ? this : this.toArray())._reverse();
  },

  reduce: function() {
    return this.length > 1 ? this : this[0];
  },

  uniq: function() {
    return this.inject([], function(array, value) {
      return array.include(value) ? array : array.concat([value]);
    });
  },

  clone: function() {
    return [].concat(this);
  },

  size: function() {
    return this.length;
  },

  inspect: function() {
    return '[' + this.map(Object.inspect).join(', ') + ']';
  }
});

Array.prototype.toArray = Array.prototype.clone;

function $w(string){
  string = string.strip();
  return string ? string.split(/\s+/) : [];
}

if(window.opera){
  Array.prototype.concat = function(){
    var array = [];
    for(var i = 0, length = this.length; i < length; i++) array.push(this[i]);
    for(var i = 0, length = arguments.length; i < length; i++) {
      if(arguments[i].constructor == Array) {
        for(var j = 0, arrayLength = arguments[i].length; j < arrayLength; j++)
          array.push(arguments[i][j]);
      } else {
        array.push(arguments[i]);
      }
    }
    return array;
  }
}
var Hash = function(obj) {
  Object.extend(this, obj || {});
};

Object.extend(Hash, {
  toQueryString: function(obj) {
    var parts = [];

	  this.prototype._each.call(obj, function(pair) {
      if (!pair.key) return;

      if (pair.value && pair.value.constructor == Array) {
        var values = pair.value.compact();
        if (values.length < 2) pair.value = values.reduce();
        else {
        	key = encodeURIComponent(pair.key);
          values.each(function(value) {
            value = value != undefined ? encodeURIComponent(value) : '';
            parts.push(key + '=' + encodeURIComponent(value));
          });
          return;
        }
      }
      if (pair.value == undefined) pair[1] = '';
      parts.push(pair.map(encodeURIComponent).join('='));
	  });

    return parts.join('&');
  }
});

Object.extend(Hash.prototype, Enumerable);
Object.extend(Hash.prototype, {
  _each: function(iterator) {
    for (var key in this) {
      var value = this[key];
      if (value && value == Hash.prototype[key]) continue;

      var pair = [key, value];
      pair.key = key;
      pair.value = value;
      iterator(pair);
    }
  },

  keys: function() {
    return this.pluck('key');
  },

  values: function() {
    return this.pluck('value');
  },

  merge: function(hash) {
    return $H(hash).inject(this, function(mergedHash, pair) {
      mergedHash[pair.key] = pair.value;
      return mergedHash;
    });
  },

  remove: function() {
    var result;
    for(var i = 0, length = arguments.length; i < length; i++) {
      var value = this[arguments[i]];
      if (value !== undefined){
        if (result === undefined) result = value;
        else {
          if (result.constructor != Array) result = [result];
          result.push(value)
        }
      }
      delete this[arguments[i]];
    }
    return result;
  },

  toQueryString: function() {
    return Hash.toQueryString(this);
  },

  inspect: function() {
    return '#<Hash:{' + this.map(function(pair) {
      return pair.map(Object.inspect).join(': ');
    }).join(', ') + '}>';
  }
});

function $H(object) {
  if (object && object.constructor == Hash) return object;
  return new Hash(object);
};
ObjectRange = Class.create();
Object.extend(ObjectRange.prototype, Enumerable);
Object.extend(ObjectRange.prototype, {
  initialize: function(start, end, exclusive) {
    this.start = start;
    this.end = end;
    this.exclusive = exclusive;
  },

  _each: function(iterator) {
    var value = this.start;
    while (this.include(value)) {
      iterator(value);
      value = value.succ();
    }
  },

  include: function(value) {
    if (value < this.start)
      return false;
    if (this.exclusive)
      return value < this.end;
    return value <= this.end;
  }
});

var $R = function(start, end, exclusive) {
  return new ObjectRange(start, end, exclusive);
}

var Ajax = {
  getTransport: function() {
    return Try.these(
      function() {return new XMLHttpRequest()},
      function() {return new ActiveXObject('Msxml2.XMLHTTP')},
      function() {return new ActiveXObject('Microsoft.XMLHTTP')}
    ) || false;
  },

  activeRequestCount: 0
}

Ajax.Responders = {
  responders: [],

  _each: function(iterator) {
    this.responders._each(iterator);
  },

  register: function(responder) {
    if (!this.include(responder))
      this.responders.push(responder);
  },

  unregister: function(responder) {
    this.responders = this.responders.without(responder);
  },

  dispatch: function(callback, request, transport, json) {
    this.each(function(responder) {
      if (typeof responder[callback] == 'function') {
        try {
          responder[callback].apply(responder, [request, transport, json]);
        } catch (e) {}
      }
    });
  }
};

Object.extend(Ajax.Responders, Enumerable);

Ajax.Responders.register({
  onCreate: function() {
    Ajax.activeRequestCount++;
  },
  onComplete: function() {
    Ajax.activeRequestCount--;
  }
});

Ajax.Base = function() {};
Ajax.Base.prototype = {
  setOptions: function(options) {
    this.options = {
      method:       'post',
      asynchronous: true,
      contentType:  'application/x-www-form-urlencoded',
      encoding:     'UTF-8',
      parameters:   ''
    }
    Object.extend(this.options, options || {});

    this.options.method = this.options.method.toLowerCase();
    if (typeof this.options.parameters == 'string')
      this.options.parameters = this.options.parameters.toQueryParams();
  }
}

Ajax.Request = Class.create();
Ajax.Request.Events =
  ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete'];

Ajax.Request.prototype = Object.extend(new Ajax.Base(), {
  _complete: false,

  initialize: function(url, options) {
    this.transport = Ajax.getTransport();
    this.setOptions(options);
    this.request(url);
  },

  request: function(url) {
    this.url = url;
    this.method = this.options.method;
    var params = this.options.parameters;

    if (!['get', 'post'].include(this.method)) {
      // simulate other verbs over post
      params['_method'] = this.method;
      this.method = 'post';
    }

    params = Hash.toQueryString(params);
    if (params && /Konqueror|Safari|KHTML/.test(navigator.userAgent)) params += '&_='

    // when GET, append parameters to URL
    if (this.method == 'get' && params)
      this.url += (this.url.indexOf('?') > -1 ? '&' : '?') + params;

    try {
      Ajax.Responders.dispatch('onCreate', this, this.transport);

      this.transport.open(this.method.toUpperCase(), this.url,
        this.options.asynchronous);

      if (this.options.asynchronous)
        setTimeout(function() { this.respondToReadyState(1) }.bind(this), 10);

      this.transport.onreadystatechange = this.onStateChange.bind(this);
      this.setRequestHeaders();

      var body = this.method == 'post' ? (this.options.postBody || params) : null;

      this.transport.send(body);

      /* Force Firefox to handle ready state 4 for synchronous requests */
      if (!this.options.asynchronous && this.transport.overrideMimeType)
        this.onStateChange();

    }
    catch (e) {
      this.dispatchException(e);
    }
  },

  onStateChange: function() {
    var readyState = this.transport.readyState;
    if (readyState > 1 && !((readyState == 4) && this._complete))
      this.respondToReadyState(this.transport.readyState);
  },

  setRequestHeaders: function() {
    var headers = {
      'X-Requested-With': 'XMLHttpRequest',
      'X-Prototype-Version': Prototype.Version,
      'Accept': 'text/javascript, text/html, application/xml, text/xml, */*'
    };

    if (this.method == 'post') {
      headers['Content-type'] = this.options.contentType +
        (this.options.encoding ? '; charset=' + this.options.encoding : '');

      /* Force "Connection: close" for older Mozilla browsers to work
       * around a bug where XMLHttpRequest sends an incorrect
       * Content-length header. See Mozilla Bugzilla #246651.
       */
      if (this.transport.overrideMimeType &&
          (navigator.userAgent.match(/Gecko\/(\d{4})/) || [0,2005])[1] < 2005)
            headers['Connection'] = 'close';
    }

    // user-defined headers
    if (typeof this.options.requestHeaders == 'object') {
      var extras = this.options.requestHeaders;

      if (typeof extras.push == 'function')
        for (var i = 0, length = extras.length; i < length; i += 2)
          headers[extras[i]] = extras[i+1];
      else
        $H(extras).each(function(pair) { headers[pair.key] = pair.value });
    }

    for (var name in headers)
      this.transport.setRequestHeader(name, headers[name]);
  },

  success: function() {
    return !this.transport.status
        || (this.transport.status >= 200 && this.transport.status < 300);
  },

  respondToReadyState: function(readyState) {
    var state = Ajax.Request.Events[readyState];
    var transport = this.transport, json = this.evalJSON();

    if (state == 'Complete') {
      try {
        this._complete = true;
        (this.options['on' + this.transport.status]
         || this.options['on' + (this.success() ? 'Success' : 'Failure')]
         || Prototype.emptyFunction)(transport, json);
      } catch (e) {
        this.dispatchException(e);
      }

      if ((this.getHeader('Content-type') || 'text/javascript').strip().
        match(/^(text|application)\/(x-)?(java|ecma)script(;.*)?$/i))
          this.evalResponse();
    }

    try {
      (this.options['on' + state] || Prototype.emptyFunction)(transport, json);
      Ajax.Responders.dispatch('on' + state, this, transport, json);
    } catch (e) {
      this.dispatchException(e);
    }

    if (state == 'Complete') {
      // avoid memory leak in MSIE: clean up
      this.transport.onreadystatechange = Prototype.emptyFunction;
    }
  },

  getHeader: function(name) {
    try {
      return this.transport.getResponseHeader(name);
    } catch (e) { return null }
  },

  evalJSON: function() {
    try {
      var json = this.getHeader('X-JSON');
      return json ? eval('(' + json + ')') : null;
    } catch (e) { return null }
  },

  evalResponse: function() {
    try {
      return eval(this.transport.responseText);
    } catch (e) {
      this.dispatchException(e);
    }
  },

  dispatchException: function(exception) {
    (this.options.onException || Prototype.emptyFunction)(this, exception);
    Ajax.Responders.dispatch('onException', this, exception);
  }
});

Ajax.Updater = Class.create();

Object.extend(Object.extend(Ajax.Updater.prototype, Ajax.Request.prototype), {
  initialize: function(container, url, options) {
    this.container = {
      success: (container.success || container),
      failure: (container.failure || (container.success ? null : container))
    }

    this.transport = Ajax.getTransport();
    this.setOptions(options);

    var onComplete = this.options.onComplete || Prototype.emptyFunction;
    this.options.onComplete = (function(transport, param) {
      this.updateContent();
      onComplete(transport, param);
    }).bind(this);

    this.request(url);
  },

  updateContent: function() {
    var receiver = this.container[this.success() ? 'success' : 'failure'];
    var response = this.transport.responseText;

    if (!this.options.evalScripts) response = response.stripScripts();

    if (receiver = $(receiver)) {
      if (this.options.insertion)
        new this.options.insertion(receiver, response);
      else
        receiver.update(response);
    }

    if (this.success()) {
      if (this.onComplete)
        setTimeout(this.onComplete.bind(this), 10);
    }
  }
});

Ajax.PeriodicalUpdater = Class.create();
Ajax.PeriodicalUpdater.prototype = Object.extend(new Ajax.Base(), {
  initialize: function(container, url, options) {
    this.setOptions(options);
    this.onComplete = this.options.onComplete;

    this.frequency = (this.options.frequency || 2);
    this.decay = (this.options.decay || 1);

    this.updater = {};
    this.container = container;
    this.url = url;

    this.start();
  },

  start: function() {
    this.options.onComplete = this.updateComplete.bind(this);
    this.onTimerEvent();
  },

  stop: function() {
    this.updater.options.onComplete = undefined;
    clearTimeout(this.timer);
    (this.onComplete || Prototype.emptyFunction).apply(this, arguments);
  },

  updateComplete: function(request) {
    if (this.options.decay) {
      this.decay = (request.responseText == this.lastText ?
        this.decay * this.options.decay : 1);

      this.lastText = request.responseText;
    }
    this.timer = setTimeout(this.onTimerEvent.bind(this),
      this.decay * this.frequency * 1000);
  },

  onTimerEvent: function() {
    this.updater = new Ajax.Updater(this.container, this.url, this.options);
  }
});
function $(element) {
  if (arguments.length > 1) {
    for (var i = 0, elements = [], length = arguments.length; i < length; i++)
      elements.push($(arguments[i]));
    return elements;
  }
  if (typeof element == 'string')
    element = document.getElementById(element);
  return Element.extend(element);
}

if (Prototype.BrowserFeatures.XPath) {
  document._getElementsByXPath = function(expression, parentElement) {
    var results = [];
    var query = document.evaluate(expression, $(parentElement) || document,
      null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
    for (var i = 0, length = query.snapshotLength; i < length; i++)
      results.push(query.snapshotItem(i));
    return results;
  };
}

document.getElementsByClassName = function(className, parentElement) {
  if (Prototype.BrowserFeatures.XPath) {
    var q = ".//*[contains(concat(' ', @class, ' '), ' " + className + " ')]";
    return document._getElementsByXPath(q, parentElement);
  } else {
    var children = ($(parentElement) || document.body).getElementsByTagName('*');
    var elements = [], child;
    for (var i = 0, length = children.length; i < length; i++) {
      child = children[i];
      if (Element.hasClassName(child, className))
        elements.push(Element.extend(child));
    }
    return elements;
  }
};

/*--------------------------------------------------------------------------*/

if (!window.Element)
  var Element = new Object();

Element.extend = function(element) {
  if (!element || _nativeExtensions || element.nodeType == 3) return element;

  if (!element._extended && element.tagName && element != window) {
    var methods = Object.clone(Element.Methods), cache = Element.extend.cache;

    if (element.tagName == 'FORM')
      Object.extend(methods, Form.Methods);
    if (['INPUT', 'TEXTAREA', 'SELECT'].include(element.tagName))
      Object.extend(methods, Form.Element.Methods);

    Object.extend(methods, Element.Methods.Simulated);

    for (var property in methods) {
      var value = methods[property];
      if (typeof value == 'function' && !(property in element))
        element[property] = cache.findOrStore(value);
    }
  }

  element._extended = true;
  return element;
};

Element.extend.cache = {
  findOrStore: function(value) {
    return this[value] = this[value] || function() {
      return value.apply(null, [this].concat($A(arguments)));
    }
  }
};

Element.Methods = {
  visible: function(element) {
    return $(element).style.display != 'none';
  },

  toggle: function(element) {
    element = $(element);
    Element[Element.visible(element) ? 'hide' : 'show'](element);
    return element;
  },

  hide: function(element) {
    $(element).style.display = 'none';
    return element;
  },

  show: function(element) {
    $(element).style.display = '';
    return element;
  },

  remove: function(element) {
    element = $(element);
    element.parentNode.removeChild(element);
    return element;
  },

  update: function(element, html) {
    html = typeof html == 'undefined' ? '' : html.toString();
    $(element).innerHTML = html.stripScripts();
    setTimeout(function() {html.evalScripts()}, 10);
    return element;
  },

  replace: function(element, html) {
    element = $(element);
    html = typeof html == 'undefined' ? '' : html.toString();
    if (element.outerHTML) {
      element.outerHTML = html.stripScripts();
    } else {
      var range = element.ownerDocument.createRange();
      range.selectNodeContents(element);
      element.parentNode.replaceChild(
        range.createContextualFragment(html.stripScripts()), element);
    }
    setTimeout(function() {html.evalScripts()}, 10);
    return element;
  },

  inspect: function(element) {
    element = $(element);
    var result = '<' + element.tagName.toLowerCase();
    $H({'id': 'id', 'className': 'class'}).each(function(pair) {
      var property = pair.first(), attribute = pair.last();
      var value = (element[property] || '').toString();
      if (value) result += ' ' + attribute + '=' + value.inspect(true);
    });
    return result + '>';
  },

  recursivelyCollect: function(element, property) {
    element = $(element);
    var elements = [];
    while (element = element[property])
      if (element.nodeType == 1)
        elements.push(Element.extend(element));
    return elements;
  },

  ancestors: function(element) {
    return $(element).recursivelyCollect('parentNode');
  },

  descendants: function(element) {
    return $A($(element).getElementsByTagName('*'));
  },

  immediateDescendants: function(element) {
    if (!(element = $(element).firstChild)) return [];
    while (element && element.nodeType != 1) element = element.nextSibling;
    if (element) return [element].concat($(element).nextSiblings());
    return [];
  },

  previousSiblings: function(element) {
    return $(element).recursivelyCollect('previousSibling');
  },

  nextSiblings: function(element) {
    return $(element).recursivelyCollect('nextSibling');
  },

  siblings: function(element) {
    element = $(element);
    return element.previousSiblings().reverse().concat(element.nextSiblings());
  },

  match: function(element, selector) {
    if (typeof selector == 'string')
      selector = new Selector(selector);
    return selector.match($(element));
  },

  up: function(element, expression, index) {
    return Selector.findElement($(element).ancestors(), expression, index);
  },

  down: function(element, expression, index) {
    return Selector.findElement($(element).descendants(), expression, index);
  },

  previous: function(element, expression, index) {
    return Selector.findElement($(element).previousSiblings(), expression, index);
  },

  next: function(element, expression, index) {
    return Selector.findElement($(element).nextSiblings(), expression, index);
  },

  getElementsBySelector: function() {
    var args = $A(arguments), element = $(args.shift());
    return Selector.findChildElements(element, args);
  },

  getElementsByClassName: function(element, className) {
    return document.getElementsByClassName(className, element);
  },

  readAttribute: function(element, name) {
    element = $(element);
    if (document.all && !window.opera) {
      var t = Element._attributeTranslations;
      if (t.values[name]) return t.values[name](element, name);
      if (t.names[name])  name = t.names[name];
      var attribute = element.attributes[name];
      if(attribute) return attribute.nodeValue;
    }
    return element.getAttribute(name);
  },

  getHeight: function(element) {
    return $(element).getDimensions().height;
  },

  getWidth: function(element) {
    return $(element).getDimensions().width;
  },

  classNames: function(element) {
    return new Element.ClassNames(element);
  },

  hasClassName: function(element, className) {
    if (!(element = $(element))) return;
    var elementClassName = element.className;
    if (elementClassName.length == 0) return false;
    if (elementClassName == className ||
        elementClassName.match(new RegExp("(^|\\s)" + className + "(\\s|$)")))
      return true;
    return false;
  },

  addClassName: function(element, className) {
    if (!(element = $(element))) return;
    Element.classNames(element).add(className);
    return element;
  },

  removeClassName: function(element, className) {
    if (!(element = $(element))) return;
    Element.classNames(element).remove(className);
    return element;
  },

  toggleClassName: function(element, className) {
    if (!(element = $(element))) return;
    Element.classNames(element)[element.hasClassName(className) ? 'remove' : 'add'](className);
    return element;
  },

  observe: function() {
    Event.observe.apply(Event, arguments);
    return $A(arguments).first();
  },

  stopObserving: function() {
    Event.stopObserving.apply(Event, arguments);
    return $A(arguments).first();
  },

  // removes whitespace-only text node children
  cleanWhitespace: function(element) {
    element = $(element);
    var node = element.firstChild;
    while (node) {
      var nextNode = node.nextSibling;
      if (node.nodeType == 3 && !/\S/.test(node.nodeValue))
        element.removeChild(node);
      node = nextNode;
    }
    return element;
  },

  empty: function(element) {
    return $(element).innerHTML.match(/^\s*$/);
  },

  descendantOf: function(element, ancestor) {
    element = $(element), ancestor = $(ancestor);
    while (element = element.parentNode)
      if (element == ancestor) return true;
    return false;
  },

  scrollTo: function(element) {
    element = $(element);
    var pos = Position.cumulativeOffset(element);
    window.scrollTo(pos[0], pos[1]);
    return element;
  },

  getStyle: function(element, style) {
    element = $(element);
    if (['float','cssFloat'].include(style))
      style = (typeof element.style.styleFloat != 'undefined' ? 'styleFloat' : 'cssFloat');
    style = style.camelize();
    var value = element.style[style];
    if (!value) {
      if (document.defaultView && document.defaultView.getComputedStyle) {
        var css = document.defaultView.getComputedStyle(element, null);
        value = css ? css[style] : null;
      } else if (element.currentStyle) {
        value = element.currentStyle[style];
      }
    }

    if((value == 'auto') && ['width','height'].include(style) && (element.getStyle('display') != 'none'))
      value = element['offset'+style.capitalize()] + 'px';

    if (window.opera && ['left', 'top', 'right', 'bottom'].include(style))
      if (Element.getStyle(element, 'position') == 'static') value = 'auto';
    if(style == 'opacity') {
      if(value) return parseFloat(value);
      if(value = (element.getStyle('filter') || '').match(/alpha\(opacity=(.*)\)/))
        if(value[1]) return parseFloat(value[1]) / 100;
      return 1.0;
    }
    return value == 'auto' ? null : value;
  },

  setStyle: function(element, style) {
    element = $(element);
    for (var name in style) {
      var value = style[name];
      if(name == 'opacity') {
        if (value == 1) {
          value = (/Gecko/.test(navigator.userAgent) &&
            !/Konqueror|Safari|KHTML/.test(navigator.userAgent)) ? 0.999999 : 1.0;
          if(/MSIE/.test(navigator.userAgent) && !window.opera)
            element.style.filter = element.getStyle('filter').replace(/alpha\([^\)]*\)/gi,'');
        } else if(value == '') {
          if(/MSIE/.test(navigator.userAgent) && !window.opera)
            element.style.filter = element.getStyle('filter').replace(/alpha\([^\)]*\)/gi,'');
        } else {
          if(value < 0.00001) value = 0;
          if(/MSIE/.test(navigator.userAgent) && !window.opera)
            element.style.filter = element.getStyle('filter').replace(/alpha\([^\)]*\)/gi,'') +
              'alpha(opacity='+value*100+')';
        }
      } else if(['float','cssFloat'].include(name)) name = (typeof element.style.styleFloat != 'undefined') ? 'styleFloat' : 'cssFloat';
      element.style[name.camelize()] = value;
    }
    return element;
  },

  getDimensions: function(element) {
    element = $(element);
    var display = $(element).getStyle('display');
    if (display != 'none' && display != null) // Safari bug
      return {width: element.offsetWidth, height: element.offsetHeight};

    // All *Width and *Height properties give 0 on elements with display none,
    // so enable the element temporarily
    var els = element.style;
    var originalVisibility = els.visibility;
    var originalPosition = els.position;
    var originalDisplay = els.display;
    els.visibility = 'hidden';
    els.position = 'absolute';
    els.display = 'block';
    var originalWidth = element.clientWidth;
    var originalHeight = element.clientHeight;
    els.display = originalDisplay;
    els.position = originalPosition;
    els.visibility = originalVisibility;
    return {width: originalWidth, height: originalHeight};
  },

  makePositioned: function(element) {
    element = $(element);
    var pos = Element.getStyle(element, 'position');
    if (pos == 'static' || !pos) {
      element._madePositioned = true;
      element.style.position = 'relative';
      // Opera returns the offset relative to the positioning context, when an
      // element is position relative but top and left have not been defined
      if (window.opera) {
        element.style.top = 0;
        element.style.left = 0;
      }
    }
    return element;
  },

  undoPositioned: function(element) {
    element = $(element);
    if (element._madePositioned) {
      element._madePositioned = undefined;
      element.style.position =
        element.style.top =
        element.style.left =
        element.style.bottom =
        element.style.right = '';
    }
    return element;
  },

  makeClipping: function(element) {
    element = $(element);
    if (element._overflow) return element;
    element._overflow = element.style.overflow || 'auto';
    if ((Element.getStyle(element, 'overflow') || 'visible') != 'hidden')
      element.style.overflow = 'hidden';
    return element;
  },

  undoClipping: function(element) {
    element = $(element);
    if (!element._overflow) return element;
    element.style.overflow = element._overflow == 'auto' ? '' : element._overflow;
    element._overflow = null;
    return element;
  }
};

Object.extend(Element.Methods, {childOf: Element.Methods.descendantOf});

Element._attributeTranslations = {};

Element._attributeTranslations.names = {
  colspan:   "colSpan",
  rowspan:   "rowSpan",
  valign:    "vAlign",
  datetime:  "dateTime",
  accesskey: "accessKey",
  tabindex:  "tabIndex",
  enctype:   "encType",
  maxlength: "maxLength",
  readonly:  "readOnly",
  longdesc:  "longDesc"
};

Element._attributeTranslations.values = {
  _getAttr: function(element, attribute) {
    return element.getAttribute(attribute, 2);
  },

  _flag: function(element, attribute) {
    return $(element).hasAttribute(attribute) ? attribute : null;
  },

  style: function(element) {
    return element.style.cssText.toLowerCase();
  },

  title: function(element) {
    var node = element.getAttributeNode('title');
    return node.specified ? node.nodeValue : null;
  }
};

Object.extend(Element._attributeTranslations.values, {
  href: Element._attributeTranslations.values._getAttr,
  src:  Element._attributeTranslations.values._getAttr,
  disabled: Element._attributeTranslations.values._flag,
  checked:  Element._attributeTranslations.values._flag,
  readonly: Element._attributeTranslations.values._flag,
  multiple: Element._attributeTranslations.values._flag
});

Element.Methods.Simulated = {
  hasAttribute: function(element, attribute) {
    var t = Element._attributeTranslations;
    attribute = t.names[attribute] || attribute;
    return $(element).getAttributeNode(attribute).specified;
  }
};

// IE is missing .innerHTML support for TABLE-related elements
if (document.all && !window.opera){
  Element.Methods.update = function(element, html) {
    element = $(element);
    html = typeof html == 'undefined' ? '' : html.toString();
    var tagName = element.tagName.toUpperCase();
    if (['THEAD','TBODY','TR','TD'].include(tagName)) {
      var div = document.createElement('div');
      switch (tagName) {
        case 'THEAD':
        case 'TBODY':
          div.innerHTML = '<table><tbody>' +  html.stripScripts() + '</tbody></table>';
          depth = 2;
          break;
        case 'TR':
          div.innerHTML = '<table><tbody><tr>' +  html.stripScripts() + '</tr></tbody></table>';
          depth = 3;
          break;
        case 'TD':
          div.innerHTML = '<table><tbody><tr><td>' +  html.stripScripts() + '</td></tr></tbody></table>';
          depth = 4;
      }
      $A(element.childNodes).each(function(node){
        element.removeChild(node)
      });
      depth.times(function(){ div = div.firstChild });

      $A(div.childNodes).each(
        function(node){ element.appendChild(node) });
    } else {
      element.innerHTML = html.stripScripts();
    }
    setTimeout(function() {html.evalScripts()}, 10);
    return element;
  }
};

Object.extend(Element, Element.Methods);

var _nativeExtensions = false;

if(/Konqueror|Safari|KHTML/.test(navigator.userAgent))
  ['', 'Form', 'Input', 'TextArea', 'Select'].each(function(tag) {
    var className = 'HTML' + tag + 'Element';
    if(window[className]) return;
    var klass = window[className] = {};
    klass.prototype = document.createElement(tag ? tag.toLowerCase() : 'div').__proto__;
  });

Element.addMethods = function(methods) {
  Object.extend(Element.Methods, methods || {});

  function copy(methods, destination, onlyIfAbsent) {
    onlyIfAbsent = onlyIfAbsent || false;
    var cache = Element.extend.cache;
    for (var property in methods) {
      var value = methods[property];
      if (!onlyIfAbsent || !(property in destination))
        destination[property] = cache.findOrStore(value);
    }
  }

  if (typeof HTMLElement != 'undefined') {
    copy(Element.Methods, HTMLElement.prototype);
    copy(Element.Methods.Simulated, HTMLElement.prototype, true);
    copy(Form.Methods, HTMLFormElement.prototype);
    [HTMLInputElement, HTMLTextAreaElement, HTMLSelectElement].each(function(klass) {
      copy(Form.Element.Methods, klass.prototype);
    });
    _nativeExtensions = true;
  }
}

var Toggle = new Object();
Toggle.display = Element.toggle;

/*--------------------------------------------------------------------------*/

Abstract.Insertion = function(adjacency) {
  this.adjacency = adjacency;
}

Abstract.Insertion.prototype = {
  initialize: function(element, content) {
    this.element = $(element);
    this.content = content.stripScripts();

    if (this.adjacency && this.element.insertAdjacentHTML) {
      try {
        this.element.insertAdjacentHTML(this.adjacency, this.content);
      } catch (e) {
        var tagName = this.element.tagName.toUpperCase();
        if (['TBODY', 'TR'].include(tagName)) {
          this.insertContent(this.contentFromAnonymousTable());
        } else {
          throw e;
        }
      }
    } else {
      this.range = this.element.ownerDocument.createRange();
      if (this.initializeRange) this.initializeRange();
      this.insertContent([this.range.createContextualFragment(this.content)]);
    }

    setTimeout(function() {content.evalScripts()}, 10);
  },

  contentFromAnonymousTable: function() {
    var div = document.createElement('div');
    div.innerHTML = '<table><tbody>' + this.content + '</tbody></table>';
    return $A(div.childNodes[0].childNodes[0].childNodes);
  }
}

var Insertion = new Object();

Insertion.Before = Class.create();
Insertion.Before.prototype = Object.extend(new Abstract.Insertion('beforeBegin'), {
  initializeRange: function() {
    this.range.setStartBefore(this.element);
  },

  insertContent: function(fragments) {
    fragments.each((function(fragment) {
      this.element.parentNode.insertBefore(fragment, this.element);
    }).bind(this));
  }
});

Insertion.Top = Class.create();
Insertion.Top.prototype = Object.extend(new Abstract.Insertion('afterBegin'), {
  initializeRange: function() {
    this.range.selectNodeContents(this.element);
    this.range.collapse(true);
  },

  insertContent: function(fragments) {
    fragments.reverse(false).each((function(fragment) {
      this.element.insertBefore(fragment, this.element.firstChild);
    }).bind(this));
  }
});

Insertion.Bottom = Class.create();
Insertion.Bottom.prototype = Object.extend(new Abstract.Insertion('beforeEnd'), {
  initializeRange: function() {
    this.range.selectNodeContents(this.element);
    this.range.collapse(this.element);
  },

  insertContent: function(fragments) {
    fragments.each((function(fragment) {
      this.element.appendChild(fragment);
    }).bind(this));
  }
});

Insertion.After = Class.create();
Insertion.After.prototype = Object.extend(new Abstract.Insertion('afterEnd'), {
  initializeRange: function() {
    this.range.setStartAfter(this.element);
  },

  insertContent: function(fragments) {
    fragments.each((function(fragment) {
      this.element.parentNode.insertBefore(fragment,
        this.element.nextSibling);
    }).bind(this));
  }
});

/*--------------------------------------------------------------------------*/

Element.ClassNames = Class.create();
Element.ClassNames.prototype = {
  initialize: function(element) {
    this.element = $(element);
  },

  _each: function(iterator) {
    this.element.className.split(/\s+/).select(function(name) {
      return name.length > 0;
    })._each(iterator);
  },

  set: function(className) {
    this.element.className = className;
  },

  add: function(classNameToAdd) {
    if (this.include(classNameToAdd)) return;
    this.set($A(this).concat(classNameToAdd).join(' '));
  },

  remove: function(classNameToRemove) {
    if (!this.include(classNameToRemove)) return;
    this.set($A(this).without(classNameToRemove).join(' '));
  },

  toString: function() {
    return $A(this).join(' ');
  }
};

Object.extend(Element.ClassNames.prototype, Enumerable);
var Selector = Class.create();
Selector.prototype = {
  initialize: function(expression) {
    this.params = {classNames: []};
    this.expression = expression.toString().strip();
    this.parseExpression();
    this.compileMatcher();
  },

  parseExpression: function() {
    function abort(message) { throw 'Parse error in selector: ' + message; }

    if (this.expression == '')  abort('empty expression');

    var params = this.params, expr = this.expression, match, modifier, clause, rest;
    while (match = expr.match(/^(.*)\[([a-z0-9_:-]+?)(?:([~\|!]?=)(?:"([^"]*)"|([^\]\s]*)))?\]$/i)) {
      params.attributes = params.attributes || [];
      params.attributes.push({name: match[2], operator: match[3], value: match[4] || match[5] || ''});
      expr = match[1];
    }

    if (expr == '*') return this.params.wildcard = true;

    while (match = expr.match(/^([^a-z0-9_-])?([a-z0-9_-]+)(.*)/i)) {
      modifier = match[1], clause = match[2], rest = match[3];
      switch (modifier) {
        case '#':       params.id = clause; break;
        case '.':       params.classNames.push(clause); break;
        case '':
        case undefined: params.tagName = clause.toUpperCase(); break;
        default:        abort(expr.inspect());
      }
      expr = rest;
    }

    if (expr.length > 0) abort(expr.inspect());
  },

  buildMatchExpression: function() {
    var params = this.params, conditions = [], clause;

    if (params.wildcard)
      conditions.push('true');
    if (clause = params.id)
      conditions.push('element.readAttribute("id") == ' + clause.inspect());
    if (clause = params.tagName)
      conditions.push('element.tagName.toUpperCase() == ' + clause.inspect());
    if ((clause = params.classNames).length > 0)
      for (var i = 0, length = clause.length; i < length; i++)
        conditions.push('element.hasClassName(' + clause[i].inspect() + ')');
    if (clause = params.attributes) {
      clause.each(function(attribute) {
        var value = 'element.readAttribute(' + attribute.name.inspect() + ')';
        var splitValueBy = function(delimiter) {
          return value + ' && ' + value + '.split(' + delimiter.inspect() + ')';
        }

        switch (attribute.operator) {
          case '=':       conditions.push(value + ' == ' + attribute.value.inspect()); break;
          case '~=':      conditions.push(splitValueBy(' ') + '.include(' + attribute.value.inspect() + ')'); break;
          case '|=':      conditions.push(
                            splitValueBy('-') + '.first().toUpperCase() == ' + attribute.value.toUpperCase().inspect()
                          ); break;
          case '!=':      conditions.push(value + ' != ' + attribute.value.inspect()); break;
          case '':
          case undefined: conditions.push('element.hasAttribute(' + attribute.name.inspect() + ')'); break;
          default:        throw 'Unknown operator ' + attribute.operator + ' in selector';
        }
      });
    }

    return conditions.join(' && ');
  },

  compileMatcher: function() {
    this.match = new Function('element', 'if (!element.tagName) return false; \
      element = $(element); \
      return ' + this.buildMatchExpression());
  },

  findElements: function(scope) {
    var element;

    if (element = $(this.params.id))
      if (this.match(element))
        if (!scope || Element.childOf(element, scope))
          return [element];

    scope = (scope || document).getElementsByTagName(this.params.tagName || '*');

    var results = [];
    for (var i = 0, length = scope.length; i < length; i++)
      if (this.match(element = scope[i]))
        results.push(Element.extend(element));

    return results;
  },

  toString: function() {
    return this.expression;
  }
}

Object.extend(Selector, {
  matchElements: function(elements, expression) {
    var selector = new Selector(expression);
    return elements.select(selector.match.bind(selector)).map(Element.extend);
  },

  findElement: function(elements, expression, index) {
    if (typeof expression == 'number') index = expression, expression = false;
    return Selector.matchElements(elements, expression || '*')[index || 0];
  },

  findChildElements: function(element, expressions) {
    return expressions.map(function(expression) {
      return expression.match(/[^\s"]+(?:"[^"]*"[^\s"]+)*/g).inject([null], function(results, expr) {
        var selector = new Selector(expr);
        return results.inject([], function(elements, result) {
          return elements.concat(selector.findElements(result || element));
        });
      });
    }).flatten();
  }
});

function $$() {
  return Selector.findChildElements(document, $A(arguments));
}
var Form = {
  reset: function(form) {
    $(form).reset();
    return form;
  },

  serializeElements: function(elements, getHash) {
    var data = elements.inject({}, function(result, element) {
      if (!element.disabled && element.name) {
        var key = element.name, value = $(element).getValue();
        if (value != undefined) {
          if (result[key]) {
            if (result[key].constructor != Array) result[key] = [result[key]];
            result[key].push(value);
          }
          else result[key] = value;
        }
      }
      return result;
    });

    return getHash ? data : Hash.toQueryString(data);
  }
};

Form.Methods = {
  serialize: function(form, getHash) {
    return Form.serializeElements(Form.getElements(form), getHash);
  },

  getElements: function(form) {
    return $A($(form).getElementsByTagName('*')).inject([],
      function(elements, child) {
        if (Form.Element.Serializers[child.tagName.toLowerCase()])
          elements.push(Element.extend(child));
        return elements;
      }
    );
  },

  getInputs: function(form, typeName, name) {
    form = $(form);
    var inputs = form.getElementsByTagName('input');

    if (!typeName && !name) return $A(inputs).map(Element.extend);

    for (var i = 0, matchingInputs = [], length = inputs.length; i < length; i++) {
      var input = inputs[i];
      if ((typeName && input.type != typeName) || (name && input.name != name))
        continue;
      matchingInputs.push(Element.extend(input));
    }

    return matchingInputs;
  },

  disable: function(form) {
    form = $(form);
    form.getElements().each(function(element) {
      element.blur();
      element.disabled = 'true';
    });
    return form;
  },

  enable: function(form) {
    form = $(form);
    form.getElements().each(function(element) {
      element.disabled = '';
    });
    return form;
  },

  findFirstElement: function(form) {
    return $(form).getElements().find(function(element) {
      return element.type != 'hidden' && !element.disabled &&
        ['input', 'select', 'textarea'].include(element.tagName.toLowerCase());
    });
  },

  focusFirstElement: function(form) {
    form = $(form);
    form.findFirstElement().activate();
    return form;
  }
}

Object.extend(Form, Form.Methods);

/*--------------------------------------------------------------------------*/

Form.Element = {
  focus: function(element) {
    $(element).focus();
    return element;
  },

  select: function(element) {
    $(element).select();
    return element;
  }
}

Form.Element.Methods = {
  serialize: function(element) {
    element = $(element);
    if (!element.disabled && element.name) {
      var value = element.getValue();
      if (value != undefined) {
        var pair = {};
        pair[element.name] = value;
        return Hash.toQueryString(pair);
      }
    }
    return '';
  },

  getValue: function(element) {
    element = $(element);
    var method = element.tagName.toLowerCase();
    return Form.Element.Serializers[method](element);
  },

  clear: function(element) {
    $(element).value = '';
    return element;
  },

  present: function(element) {
    return $(element).value != '';
  },

  activate: function(element) {
    element = $(element);
    element.focus();
    if (element.select && ( element.tagName.toLowerCase() != 'input' ||
      !['button', 'reset', 'submit'].include(element.type) ) )
      element.select();
    return element;
  },

  disable: function(element) {
    element = $(element);
    element.disabled = true;
    return element;
  },

  enable: function(element) {
    element = $(element);
    element.blur();
    element.disabled = false;
    return element;
  }
}

Object.extend(Form.Element, Form.Element.Methods);
var Field = Form.Element;
var $F = Form.Element.getValue;

/*--------------------------------------------------------------------------*/

Form.Element.Serializers = {
  input: function(element) {
    switch (element.type.toLowerCase()) {
      case 'checkbox':
      case 'radio':
        return Form.Element.Serializers.inputSelector(element);
      default:
        return Form.Element.Serializers.textarea(element);
    }
  },

  inputSelector: function(element) {
    return element.checked ? element.value : null;
  },

  textarea: function(element) {
    return element.value;
  },

  select: function(element) {
    return this[element.type == 'select-one' ?
      'selectOne' : 'selectMany'](element);
  },

  selectOne: function(element) {
    var index = element.selectedIndex;
    return index >= 0 ? this.optionValue(element.options[index]) : null;
  },

  selectMany: function(element) {
    var values, length = element.length;
    if (!length) return null;

    for (var i = 0, values = []; i < length; i++) {
      var opt = element.options[i];
      if (opt.selected) values.push(this.optionValue(opt));
    }
    return values;
  },

  optionValue: function(opt) {
    // extend element because hasAttribute may not be native
    return Element.extend(opt).hasAttribute('value') ? opt.value : opt.text;
  }
}

/*--------------------------------------------------------------------------*/

Abstract.TimedObserver = function() {}
Abstract.TimedObserver.prototype = {
  initialize: function(element, frequency, callback) {
    this.frequency = frequency;
    this.element   = $(element);
    this.callback  = callback;

    this.lastValue = this.getValue();
    this.registerCallback();
  },

  registerCallback: function() {
    setInterval(this.onTimerEvent.bind(this), this.frequency * 1000);
  },

  onTimerEvent: function() {
    var value = this.getValue();
    var changed = ('string' == typeof this.lastValue && 'string' == typeof value
      ? this.lastValue != value : String(this.lastValue) != String(value));
    if (changed) {
      this.callback(this.element, value);
      this.lastValue = value;
    }
  }
}

Form.Element.Observer = Class.create();
Form.Element.Observer.prototype = Object.extend(new Abstract.TimedObserver(), {
  getValue: function() {
    return Form.Element.getValue(this.element);
  }
});

Form.Observer = Class.create();
Form.Observer.prototype = Object.extend(new Abstract.TimedObserver(), {
  getValue: function() {
    return Form.serialize(this.element);
  }
});

/*--------------------------------------------------------------------------*/

Abstract.EventObserver = function() {}
Abstract.EventObserver.prototype = {
  initialize: function(element, callback) {
    this.element  = $(element);
    this.callback = callback;

    this.lastValue = this.getValue();
    if (this.element.tagName.toLowerCase() == 'form')
      this.registerFormCallbacks();
    else
      this.registerCallback(this.element);
  },

  onElementEvent: function() {
    var value = this.getValue();
    if (this.lastValue != value) {
      this.callback(this.element, value);
      this.lastValue = value;
    }
  },

  registerFormCallbacks: function() {
    Form.getElements(this.element).each(this.registerCallback.bind(this));
  },

  registerCallback: function(element) {
    if (element.type) {
      switch (element.type.toLowerCase()) {
        case 'checkbox':
        case 'radio':
          Event.observe(element, 'click', this.onElementEvent.bind(this));
          break;
        default:
          Event.observe(element, 'change', this.onElementEvent.bind(this));
          break;
      }
    }
  }
}

Form.Element.EventObserver = Class.create();
Form.Element.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), {
  getValue: function() {
    return Form.Element.getValue(this.element);
  }
});

Form.EventObserver = Class.create();
Form.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), {
  getValue: function() {
    return Form.serialize(this.element);
  }
});
if (!window.Event) {
  var Event = new Object();
}

Object.extend(Event, {
  KEY_BACKSPACE: 8,
  KEY_TAB:       9,
  KEY_RETURN:   13,
  KEY_ESC:      27,
  KEY_LEFT:     37,
  KEY_UP:       38,
  KEY_RIGHT:    39,
  KEY_DOWN:     40,
  KEY_DELETE:   46,
  KEY_HOME:     36,
  KEY_END:      35,
  KEY_PAGEUP:   33,
  KEY_PAGEDOWN: 34,

  element: function(event) {
    return event.target || event.srcElement;
  },

  isLeftClick: function(event) {
    return (((event.which) && (event.which == 1)) ||
            ((event.button) && (event.button == 1)));
  },

  pointerX: function(event) {
    return event.pageX || (event.clientX +
      (document.documentElement.scrollLeft || document.body.scrollLeft));
  },

  pointerY: function(event) {
    return event.pageY || (event.clientY +
      (document.documentElement.scrollTop || document.body.scrollTop));
  },

  stop: function(event) {
    if (event.preventDefault) {
      event.preventDefault();
      event.stopPropagation();
    } else {
      event.returnValue = false;
      event.cancelBubble = true;
    }
  },

  // find the first node with the given tagName, starting from the
  // node the event was triggered on; traverses the DOM upwards
  findElement: function(event, tagName) {
    var element = Event.element(event);
    while (element.parentNode && (!element.tagName ||
        (element.tagName.toUpperCase() != tagName.toUpperCase())))
      element = element.parentNode;
    return element;
  },

  observers: false,

  _observeAndCache: function(element, name, observer, useCapture) {
    if (!this.observers) this.observers = [];
    if (element.addEventListener) {
      this.observers.push([element, name, observer, useCapture]);
      element.addEventListener(name, observer, useCapture);
    } else if (element.attachEvent) {
      this.observers.push([element, name, observer, useCapture]);
      element.attachEvent('on' + name, observer);
    }
  },

  unloadCache: function() {
    if (!Event.observers) return;
    for (var i = 0, length = Event.observers.length; i < length; i++) {
      Event.stopObserving.apply(this, Event.observers[i]);
      Event.observers[i][0] = null;
    }
    Event.observers = false;
  },

  observe: function(element, name, observer, useCapture) {
    element = $(element);
    useCapture = useCapture || false;

    if (name == 'keypress' &&
        (navigator.appVersion.match(/Konqueror|Safari|KHTML/)
        || element.attachEvent))
      name = 'keydown';

    Event._observeAndCache(element, name, observer, useCapture);
  },

  stopObserving: function(element, name, observer, useCapture) {
    element = $(element);
    useCapture = useCapture || false;

    if (name == 'keypress' &&
        (navigator.appVersion.match(/Konqueror|Safari|KHTML/)
        || element.detachEvent))
      name = 'keydown';

    if (element.removeEventListener) {
      element.removeEventListener(name, observer, useCapture);
    } else if (element.detachEvent) {
      try {
        element.detachEvent('on' + name, observer);
      } catch (e) {}
    }
  }
});

/* prevent memory leaks in IE */
if (navigator.appVersion.match(/\bMSIE\b/))
  Event.observe(window, 'unload', Event.unloadCache, false);
var Position = {
  // set to true if needed, warning: firefox performance problems
  // NOT neeeded for page scrolling, only if draggable contained in
  // scrollable elements
  includeScrollOffsets: false,

  // must be called before calling withinIncludingScrolloffset, every time the
  // page is scrolled
  prepare: function() {
    this.deltaX =  window.pageXOffset
                || document.documentElement.scrollLeft
                || document.body.scrollLeft
                || 0;
    this.deltaY =  window.pageYOffset
                || document.documentElement.scrollTop
                || document.body.scrollTop
                || 0;
  },

  realOffset: function(element) {
    var valueT = 0, valueL = 0;
    do {
      valueT += element.scrollTop  || 0;
      valueL += element.scrollLeft || 0;
      element = element.parentNode;
    } while (element);
    return [valueL, valueT];
  },

  cumulativeOffset: function(element) {
    var valueT = 0, valueL = 0;
    do {
      valueT += element.offsetTop  || 0;
      valueL += element.offsetLeft || 0;
      element = element.offsetParent;
    } while (element);
    return [valueL, valueT];
  },

  positionedOffset: function(element) {
    var valueT = 0, valueL = 0;
    do {
      valueT += element.offsetTop  || 0;
      valueL += element.offsetLeft || 0;
      element = element.offsetParent;
      if (element) {
        if(element.tagName=='BODY') break;
        var p = Element.getStyle(element, 'position');
        if (p == 'relative' || p == 'absolute') break;
      }
    } while (element);
    return [valueL, valueT];
  },

  offsetParent: function(element) {
    if (element.offsetParent) return element.offsetParent;
    if (element == document.body) return element;

    while ((element = element.parentNode) && element != document.body)
      if (Element.getStyle(element, 'position') != 'static')
        return element;

    return document.body;
  },

  // caches x/y coordinate pair to use with overlap
  within: function(element, x, y) {
    if (this.includeScrollOffsets)
      return this.withinIncludingScrolloffsets(element, x, y);
    this.xcomp = x;
    this.ycomp = y;
    this.offset = this.cumulativeOffset(element);

    return (y >= this.offset[1] &&
            y <  this.offset[1] + element.offsetHeight &&
            x >= this.offset[0] &&
            x <  this.offset[0] + element.offsetWidth);
  },

  withinIncludingScrolloffsets: function(element, x, y) {
    var offsetcache = this.realOffset(element);

    this.xcomp = x + offsetcache[0] - this.deltaX;
    this.ycomp = y + offsetcache[1] - this.deltaY;
    this.offset = this.cumulativeOffset(element);

    return (this.ycomp >= this.offset[1] &&
            this.ycomp <  this.offset[1] + element.offsetHeight &&
            this.xcomp >= this.offset[0] &&
            this.xcomp <  this.offset[0] + element.offsetWidth);
  },

  // within must be called directly before
  overlap: function(mode, element) {
    if (!mode) return 0;
    if (mode == 'vertical')
      return ((this.offset[1] + element.offsetHeight) - this.ycomp) /
        element.offsetHeight;
    if (mode == 'horizontal')
      return ((this.offset[0] + element.offsetWidth) - this.xcomp) /
        element.offsetWidth;
  },

  page: function(forElement) {
    var valueT = 0, valueL = 0;

    var element = forElement;
    do {
      valueT += element.offsetTop  || 0;
      valueL += element.offsetLeft || 0;

      // Safari fix
      if (element.offsetParent==document.body)
        if (Element.getStyle(element,'position')=='absolute') break;

    } while (element = element.offsetParent);

    element = forElement;
    do {
      if (!window.opera || element.tagName=='BODY') {
        valueT -= element.scrollTop  || 0;
        valueL -= element.scrollLeft || 0;
      }
    } while (element = element.parentNode);

    return [valueL, valueT];
  },

  clone: function(source, target) {
    var options = Object.extend({
      setLeft:    true,
      setTop:     true,
      setWidth:   true,
      setHeight:  true,
      offsetTop:  0,
      offsetLeft: 0
    }, arguments[2] || {})

    // find page position of source
    source = $(source);
    var p = Position.page(source);

    // find coordinate system to use
    target = $(target);
    var delta = [0, 0];
    var parent = null;
    // delta [0,0] will do fine with position: fixed elements,
    // position:absolute needs offsetParent deltas
    if (Element.getStyle(target,'position') == 'absolute') {
      parent = Position.offsetParent(target);
      delta = Position.page(parent);
    }

    // correct by body offsets (fixes Safari)
    if (parent == document.body) {
      delta[0] -= document.body.offsetLeft;
      delta[1] -= document.body.offsetTop;
    }

    // set position
    if(options.setLeft)   target.style.left  = (p[0] - delta[0] + options.offsetLeft) + 'px';
    if(options.setTop)    target.style.top   = (p[1] - delta[1] + options.offsetTop) + 'px';
    if(options.setWidth)  target.style.width = source.offsetWidth + 'px';
    if(options.setHeight) target.style.height = source.offsetHeight + 'px';
  },

  absolutize: function(element) {
    element = $(element);
    if (element.style.position == 'absolute') return;
    Position.prepare();

    var offsets = Position.positionedOffset(element);
    var top     = offsets[1];
    var left    = offsets[0];
    var width   = element.clientWidth;
    var height  = element.clientHeight;

    element._originalLeft   = left - parseFloat(element.style.left  || 0);
    element._originalTop    = top  - parseFloat(element.style.top || 0);
    element._originalWidth  = element.style.width;
    element._originalHeight = element.style.height;

    element.style.position = 'absolute';
    element.style.top    = top + 'px';
    element.style.left   = left + 'px';
    element.style.width  = width + 'px';
    element.style.height = height + 'px';
  },

  relativize: function(element) {
    element = $(element);
    if (element.style.position == 'relative') return;
    Position.prepare();

    element.style.position = 'relative';
    var top  = parseFloat(element.style.top  || 0) - (element._originalTop || 0);
    var left = parseFloat(element.style.left || 0) - (element._originalLeft || 0);

    element.style.top    = top + 'px';
    element.style.left   = left + 'px';
    element.style.height = element._originalHeight;
    element.style.width  = element._originalWidth;
  }
}

// Safari returns margins on body which is incorrect if the child is absolutely
// positioned.  For performance reasons, redefine Position.cumulativeOffset for
// KHTML/WebKit only.
if (/Konqueror|Safari|KHTML/.test(navigator.userAgent)) {
  Position.cumulativeOffset = function(element) {
    var valueT = 0, valueL = 0;
    do {
      valueT += element.offsetTop  || 0;
      valueL += element.offsetLeft || 0;
      if (element.offsetParent == document.body)
        if (Element.getStyle(element, 'position') == 'absolute') break;

      element = element.offsetParent;
    } while (element);

    return [valueL, valueT];
  }
}

Element.addMethods();
//</nowiki></pre></div></div>
<!-- --><div class="NavFrame" style="text-align: left; background-color: #dde;"><div class="NavHead" style="display: inline; padding-right: 4em; background-color: transparent;">I18nManager.js</div><div class="NavContent" style="text-align: left"><pre><nowiki>
/*
    This class handles internationalization.

    I18nManager is intended to be used through:
            _.KEY
    where "KEY" is an internationalization key and "_" is the short for
    I18nManager.bundle.en (or whatever language the user has chosen).
    "_.KEY" will give the internationalized string.

    The convention I use for parameterized strings is inspired by Java.
    For instance, the value:
            s = "User {0} overwrote {1} of your edits."
    whould be filled in by using:
            s.replace(/\{0\}/g, someUsername).replace(/\{1\}/g, nEdits)
*/

var I18nManager = {

bundle: {

en: { // English language

BUTTON_OK: "OK",
BUTTON_CANCEL: "Cancel",
BUTTON_CLOSE: "Close",
BUTTON_SHOWALL: "Show All",
BUTTON_REFRESH: "Refresh",
BUTTON_EDIT: "Edit",
BUTTON_ADD: "Add",
BUTTON_REMOVE: "Remove",
BUTTON_RESCAN: "Re-scan",
BUTTON_FIX: "Fix",
BUTTON_MYUSERSCRIPTS: "My User Scripts",
LABEL_CONTACTS: "Contacts",
LABEL_EMPTY: "Empty.",
LABEL_ONESUGGESTION: "1 suggestion: ",
LABEL_SUGGESTIONS: " suggestions: ",
LABEL_ADDTOSUMMARY: "Add to summary",
LABEL_CLICKTOFILLSUMMARY: "Click to fill the proposed summary into the input field below",
LABEL_TALK: "Talk",
LABEL_CONTRIBS: "Contribs",
INFO_REFRESHING: "Refreshing...",
INFO_NOSUGGESTIONS: "No suggestions.",
INFO_REPEATANALYSIS: "Repeat the analysis of the text",
INFO_PREFSINCOOKIE: "Your selection of user scripts will be remembered in a cookie.  \
                     If you use a different computer or a different browser, you have to configure them again.",
INFO_PLUGIN_ADVISOR: "Advisor (suggests replacements while you edit)",
INFO_PLUGIN_CONTACTLIST: "Contact list (monitors the online presence of selected users)",
INFO_PLUGIN_ZOCKYLINKCOMPLETE: "Zocky's \"Link Complete\" (suggests names of existing articles after \
                                you type \"[[foo\" and press TAB)",
INFO_PLUGIN_CYRILLIC: "Helps display Cyrillic URL-s in the address bar",
INFO_PLUGIN_SUBSTITUTIONS: "Replaces sequences of keystrokes with certain strings, similar to VIM's \":map\" command.",
ERROR_APPLICABLETEXTCHANGED: "Error - The applicable text has changed.  Fix failed.",
ERROR_EDITSUMMARYLIMIT: "Error - If the proposed text is added to the summary, \
    its length will exceed the {0}-character maximum by {1} characters.",
DUMMY: ""

},

bg: { // Bulgarian language

BUTTON_OK: "ОК",
BUTTON_CANCEL: "Отказ",
BUTTON_CLOSE: "Затвори",
BUTTON_SHOWALL: "Покажи Всички",
BUTTON_REFRESH: "Обнови",
BUTTON_EDIT: "Редактирай",
BUTTON_ADD: "Добави",
BUTTON_REMOVE: "Махни",
BUTTON_RESCAN: "Провери Пак",
BUTTON_FIX: "Поправи",
BUTTON_MYUSERSCRIPTS: "Моите Скриптове",
LABEL_CONTACTS: "Контакти",
LABEL_EMPTY: "Няма.",
LABEL_ONESUGGESTION: "1 предложение: ",
LABEL_SUGGESTIONS: " предложения: ",
LABEL_ADDTOSUMMARY: "Добави към описанието",
LABEL_CLICKTOFILLSUMMARY: "Добави предложеното описание към долното поле",
LABEL_TALK: "Беседка",
LABEL_CONTRIBS: "Приноси",
INFO_REFRESHING: "Обновяване...",
INFO_NOSUGGESTIONS: "Няма предложения.",
INFO_REPEATANALYSIS: "Повтори анализа на текста",
INFO_PREFSINCOOKIE: "Вашият избор на скриптове ще бъде запомнен в cookie.  \
                     Ако ползвате друг компютър или друг браузър, ще трябва пак да ги конфигурирате.",
INFO_PLUGIN_ADVISOR: "Advisor (предлага промени на части от текста докато редактирате)",
INFO_PLUGIN_CONTACTLIST: "Contact list (наблюдава онлайн-присъствието на избрани потребители)",
INFO_PLUGIN_ZOCKYLINKCOMPLETE: "Zocky's \"Link Complete\" (предлага имена съществуващи статии след \
                                като напишете \"[[нещо\" и натиснете TAB)",
INFO_PLUGIN_CYRILLIC: "Помага да се показват кирилски адреси в адресната лента на браузъра",
INFO_PLUGIN_SUBSTITUTIONS: "Подменя някои поредици от клавиши с определени низове, подобно на командата \":map\" във VIM",
ERROR_APPLICABLETEXTCHANGED: "Грешка - Съответният текст е променен.  Поправянето е неуспешно.",
ERROR_EDITSUMMARYLIMIT: "Грешка - Ако предложеният текст се добави към описанието, \
    дължината му ще надвиши {0}-символното ограничение с {1} символа.",
DUMMY: ""

}

}
};

var _ = I18nManager.bundle[window.wgUserLanguage || "en"] // in case wgUserLanguage is not set
        || I18nManager.bundle.en; // in case we don't have a bundle for the user's language


// todo: Add a sanity self-check that every internationalization key is translated into every language
//</nowiki></pre></div></div>
<!-- --><div class="NavFrame" style="text-align: left; background-color: #dde;"><div class="NavHead" style="display: inline; padding-right: 4em; background-color: transparent;">DOMExtensions.js</div><div class="NavContent" style="text-align: left"><pre><nowiki>
/*
    These are shortcuts for frequently-used operations on DOM nodes.   
*/

function $elem(tagName/*, Element... children*/) {
    var e = $(document.createElement(tagName));
    if (arguments.length > 1) {
        e.appendChildren.apply(e, $A(arguments).splice(1));
    }
    return e;
}

/**
 * The second parameter is optional and can be either a function (attached as an
 * "onclick" observer) or a string (used as a "href" attribute).
 *
 * "title" is optional, too.
 */
function $anchor(text, hrefOrOnclick, title) {
    var e = $elem("A", text);
    e.title = title || null;
    if ((typeof hrefOrOnclick) == "string") {
        e.href = hrefOrOnclick;
    } else if ((typeof hrefOrOnclick) == "function") {
        e.href = "javascript: void(0);";
        Event.observe(e, "click", function (hrefOrOnclick) {
            hrefOrOnclick();
            return false;
        }.bind(null, hrefOrOnclick));
    } else {
        e.href = "#";
    }
    return e;
}

Element.addMethods({

    removeAllChildren: function (e) {
        while (e.firstChild) {
            e.removeChild(e.firstChild);
        }
        return e;
    },

    appendText: function (e, text) {
        e.appendChild(document.createTextNode(text));
        return e;
    },

    appendChildren: function (e) {
        for (var i = 1; i < arguments.length; i++) {
            var x = arguments[i];
            e.appendChild(((typeof x) == "string") ? document.createTextNode(x) : x);
        }
        return e;
    },

    scrollToSelection: function (e) {
        if (e.nodeName != "TEXTAREA") {
            throw new Exception("scrollToSelection() is available only for TEXTAREA-s.");
        }
        var value = e.value;
        var selectionStart = e.selectionStart;
        var selectionEnd = e.selectionEnd;
        var pad = "";
        var h = "\n";
        var x = e.rows;
        while (x != 0) {
            if (x & 1) {
                pad += h;
            }
            x >>>= 1;
            h += h;
        }
        e.value = pad + value.substring(0, selectionStart);
        var yOffset = e.scrollHeight;
        e.value = value;
        e.selectionStart = selectionStart;
        e.selectionEnd = selectionEnd;
        e.scrollTop = (yOffset > e.clientHeight) ? (yOffset - (3 * e.clientHeight / 2)) : 0;
        return e;
    }
});
//</nowiki></pre></div></div>
<!-- --><div class="NavFrame" style="text-align: left; background-color: #dde;"><div class="NavHead" style="display: inline; padding-right: 4em; background-color: transparent;">RegExpExtensions.js</div><div class="NavContent" style="text-align: left"><pre><nowiki>
/*
    Regular expressions can sometimes become inconveniently large.
    In order to make complex ones easier to understand, we introduce
    a set of macros. Names enclosed with '{' and '}' will be replaced
    according to the hashtable below.

    In order to do the replacements, one must call .fix() on the 
    regex object and use the result instead, like this:

        var re = /It happened in {month}/.fix();

    Also, for the sake of convenience, we add the "getAllMatches(s)"
    method, which is a quick means to find all occurrences of a
    regex in some text. The result is an array containing the results
    of applying RegExp.exec(..).
*/

RegExp.REPLACEMENTS = {
    letter: // all Unicode letters
            // http://www.codeproject.com/dotnet/UnicodeCharCatHelper.asp
            "\\u0041-\\u005a\\u0061-\\u007a\\u00aa"
            + "\\u00b5\\u00ba\\u00c0-\\u00d6"
            + "\\u00d8-\\u00f6\\u00f8-\\u01ba\\u01bc-\\u01bf"
            + "\\u01c4-\\u02ad\\u0386\\u0388-\\u0481\\u048c-\\u0556"
            + "\\u0561-\\u0587\\u10a0-\\u10c5\\u1e00-\\u1fbc\\u1fbe"
            + "\\u1fc2-\\u1fcc\\u1fd0-\\u1fdb\\u1fe0-\\u1fec"
            + "\\u1ff2-\\u1ffc\\u207f\\u2102\\u2107\\u210a-\\u2113"
            + "\\u2115\\u2119-\\u211d\\u2124\\u2126\\u2128"
            + "\\u212a-\\u212d\\u212f-\\u2131\\u2133\\u2134\\u2139"
            + "\\ufb00-\\ufb17\\uff21-\\uff3a\\uff41-\\uff5a",
    letter_bg: // note that this is not the full set of Cyrillic letters
            "АБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЬЮЯабвгдежзийклмнопрстуфхцчшщъьюя",
    month:  //
            "(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec|"
            + "January|February|March|April|June|July|August|September|"
            + "October|November|December)"
};

RegExp.escape = function (s) {
    var r = "";
    var hex = "0123456789abcdef";
    for (var i = 0; i < s.length; i++) {
        var ch = s.charCodeAt(i);
        r += "\\u" + hex[(ch >>> 12) & 0x0f]
                   + hex[(ch >>> 8) & 0x0f]
                   + hex[(ch >>> 4) & 0x0f]
                   + hex[ch & 0x0f];
    }
    return r;
};

RegExp.prototype.getAllMatches = function (s) { // : Match[]
    var p = 0;
    var a = [];
    while (true) {
        this.lastIndex = 0;
        var m = this.exec(s.substring(p));
        if (m == null) {
            return a;
        }
        m.start = p + m.index;
        m.end = p + m.index + m[0].length;
        a.push(m);
        p = m.end;
    }
};

RegExp.prototype.fix = function () { // : RegExp
    if (this.fixedRE != null) {
        return this.fixedRE;
    }
    var s = this.source;
    for (var alias in RegExp.REPLACEMENTS) {
        s = s.replace(
                new RegExp("{" + alias + "}", "g"),
                RegExp.REPLACEMENTS[alias]
        );
    }
    var re = new RegExp(s);
    re.global = this.global;
    re.ignoreCase = this.ignoreCase;
    re.multiline = this.multiline;
    this.fixedRE = re; // the fixed copy is cached
    return re;
};
//</nowiki></pre></div></div>
<!-- --><div class="NavFrame" style="text-align: left; background-color: #dde;"><div class="NavHead" style="display: inline; padding-right: 4em; background-color: transparent;">CookieManager.js</div><div class="NavContent" style="text-align: left"><pre><nowiki>

var CookieManager = Class.create();

CookieManager.getCookie = function (name) {
    var a = document.cookie.split(/; +/);
    var prefix = escape(name) + "=";
    for (var i = 0; i < a.length; i++) {
        if (a[i].substring(0, prefix.length) == prefix) {
            return unescape(a[i].substring(prefix.length));
        }
    }
    return null;
};

CookieManager.setCookie = function (name, value) {
    if ((value == null) || ("" + value == "")) {
        CookieManager.deleteCookie(name);
        return;
    }
    var expirationDate = new Date(new Date().getTime() + (5 * 365 * 24 * 60 * 60 * 1000)).toGMTString(); // in 5 years
    document.cookie = escape(name) + "=" + escape(value) + "; Expires=" + expirationDate + "; Path=/;";
};

CookieManager.deleteCookie = function (name) {
    document.cookie = escape(name) + "=; Expires=" + new Date().toGMTString() + "; Path=/;";
};
//</nowiki></pre></div></div>
<!-- --><div class="NavFrame" style="text-align: left; background-color: #dde;"><div class="NavHead" style="display: inline; padding-right: 4em; background-color: transparent;">ModalDialogManager.js</div><div class="NavContent" style="text-align: left"><pre><nowiki>
/*
    Displays a DOM node on top of the rest of the page.
    The rest of the page is visible (or at least semi-transparent), but not clickable. 
*/

var ModalDialogManager = Class.create();

ModalDialogManager.show = function (e) {
    if (ModalDialogManager.dimmer != null) {
        return false;
    }
    var whitebox = $elem("DIV", e);
    Object.extend(whitebox.style, {
        position: "fixed",
        backgroundColor: "white",
        border: "solid black 1px",
        padding: "1em",
        left: Math.floor(innerWidth / 6) + "px",
        top: Math.floor(innerHeight / 6) + "px",
        width: Math.floor(innerWidth * 2 / 3) + "px",
        height: Math.floor(innerHeight * 2 / 3) + "px",
        overflow: "scroll"
    });
    var dimmer = ModalDialogManager.dimmer = $elem("DIV", whitebox);
    Object.extend(dimmer.style, {
        position: "absolute",
        width: document.body.clientWidth + "px",
        height: document.body.clientHeight + "px",
        left: "0",
        top: "0",
        zIndex: 999,
        textAlign: "center"
/*
        , // transparency seems to slow rendering down too much
        filter: "alpha(opacity=60)",
        "-moz-opacity": 0.6,
        opacity: 0.6,
        backgroundColor: "grey"
*/
    });
    document.body.appendChild(dimmer);
    return true;
};

ModalDialogManager.hide = function () {
    if (ModalDialogManager.dimmer != null) {
        ModalDialogManager.dimmer.remove();
        ModalDialogManager.dimmer = null;
    }
};
//</nowiki></pre></div></div>
<!-- --><div class="NavFrame" style="text-align: left; background-color: #dde;"><div class="NavHead" style="display: inline; padding-right: 4em; background-color: transparent;">MediawikiManager.js</div><div class="NavContent" style="text-align: left"><pre><nowiki>
/*
    Hides mediawiki quirks behind a convenient interface
    and encapsulates interaction with the surrounding DOM.

    Some of my classes live happily without using MediawikiManager.
    I thought it is a good idea to encapsulate at least some of the interaction, though.
*/

var MediawikiManager = {};

MediawikiManager.isPreferencesPage = function () {
    return ($("preferences") != null);
};

MediawikiManager.addPreferenceTab = function (e) {
    // Assuming we are at the "preferences" page now, we must first add our stuff.
    // the "tabbedprefs()" function has already messed things up.
    // We'll just undo some of its actions, and make it work again
    // and include our new tab
    var index = $("preftoc").childNodes.length;
    $("preferences").insertBefore($elem("FIELDSET", $elem("LEGEND", _.BUTTON_MYUSERSCRIPTS), e), $("prefcontrol"));
    $("preftoc").remove(); // tabbedprefs() will re-generate the toc
    $("prefcontrol").id = "prefsubmit"; // tabbedprefs() will swap the id-s back
    tabbedprefs();
};

MediawikiManager.observePreferencesSubmit = function (f) {
    Event.observe($("prefcontrol").descendants("INPUT")[0], "click", f);
};

MediawikiManager.importScript = function (s) {
    importScript.apply(window, arguments);
};
//</nowiki></pre></div></div>
<!-- --><div class="NavFrame" style="text-align: left; background-color: #dde;"><div class="NavHead" style="display: inline; padding-right: 4em; background-color: transparent;">CameltraderPluginManager.js</div><div class="NavContent" style="text-align: left"><pre><nowiki>

var CameltraderPluginManager = Class.create();

// Static info about plugins
CameltraderPluginManager.PLUGINS = $H({
    advisor: {
        description: _.INFO_PLUGIN_ADVISOR,
        implementation: function () { /*new AdvisorHighlighterPlugin(*/new AdvisorPlugin()/*)*/; }
    },
    contactList: {
        description: _.INFO_PLUGIN_CONTACTLIST,
        implementation: function () { new ContactListPlugin(); }
    },
    zockyLinkComplete: {
        description: _.INFO_PLUGIN_ZOCKYLINKCOMPLETE,
        implementation: function () { new ZockyLinkCompletePlugin(); }
    },
/*
    lupinPopups: {
        description: "User:Lupin's popups.js",
        implementation: function () { new LupinPopupsPlugin(); }
    },
    cacycleDiff: {
        description: "User:Cacycle's diff.js",
        implementation: function () { new CacycleDiffPlugin(); }
    },
*/
    cyrillic: {
        description: _.INFO_PLUGIN_CYRILLIC,
        implementation: function () { new CyrillicPlugin(); }
    },
    substitutions: {
        description: _.INFO_PLUGIN_SUBSTITUTIONS,
        implementation: function () { new SubstitutionsPlugin(); }
    }
});


CameltraderPluginManager.prototype = {

    initialize: function () {
        CameltraderPluginManager.INSTANCE = this;
        this.aConfiguredPluginNames = (CookieManager.getCookie("plugins") || "").split(",").compact().uniq();
        this.hConfiguredPluginNames = $H();
        this.aConfiguredPluginNames._each(function (name) {
            if (CameltraderPluginManager.PLUGINS[name] == null) {
                return;
            }
            this.hConfiguredPluginNames[name] = true;
            CameltraderPluginManager.PLUGINS[name].implementation();
        }.bind(this));
        if (!MediawikiManager.isPreferencesPage()) {
            return;
        }
        this.hSumbittablePluginNames = $H().merge(this.hConfiguredPluginNames);
        var eDiv = $elem("DIV", _.INFO_PREFSINCOOKIE);
        CameltraderPluginManager.PLUGINS.keys()._each(function (name) {
            var eCheckbox = $elem("INPUT");
            eCheckbox.id = "checkbox-" + name;
            eCheckbox.type = "checkbox";
            if (this.hConfiguredPluginNames[name]) {
                eCheckbox.checked = true;
            }
            var eLabel = $elem("LABEL", CameltraderPluginManager.PLUGINS[name].description);
            eLabel.htmlFor = eCheckbox.id;
            eLabel.id = "label-" + name;
            Event.observe(eCheckbox, "click", function (name, eCheckbox, eLabel) {
                if (eCheckbox.checked) {
                    this.hSumbittablePluginNames[name] = true;
                } else {
                    this.hSumbittablePluginNames.remove(name);
                }
                var isConfigured = !!this.hConfiguredPluginNames[name];
                if (eCheckbox.checked && !isConfigured) {
                    eLabel.style.backgroundColor = "#afa"; // green
                } else if (isConfigured && !eCheckbox.checked) {
                    eLabel.style.backgroundColor = "#faa"; // red
                } else {
                    eLabel.style.backgroundColor = "";
                }
            }.bind(this, name, eCheckbox, eLabel));
            eDiv.appendChild($elem("DIV", eCheckbox, eLabel));
        }.bind(this));
        MediawikiManager.addPreferenceTab(eDiv);
        MediawikiManager.observePreferencesSubmit(function () {
            var value = this.hSumbittablePluginNames.keys().join(",");
            CookieManager.setCookie("plugins", value);
        }.bind(this));
    }
};
//</nowiki></pre></div></div>
<!-- --><div class="NavFrame" style="text-align: left; background-color: #dde;"><div class="NavHead" style="display: inline; padding-right: 4em; background-color: transparent;">AdvisorPlugin.js</div><div class="NavContent" style="text-align: left"><pre><nowiki>

var AdvisorPlugin = Class.create();
AdvisorPlugin.DEFAULT_MAX_SUGGESTIONS = 8;

AdvisorPlugin.prototype = {

    initialize: function () {
        this.wpTextbox1 = $("wpTextbox1");
        this.wpSummary = $("wpSummary");
        var editform = $("editform");
        var wpSummaryLabel = $("wpSummaryLabel");
        //
        this.maxSuggestions = AdvisorPlugin.DEFAULT_MAX_SUGGESTIONS;
        this.suggestions = null; // : AdvisorSuggestion[]
        this.appliedSuggestions = {}; // : Map<String, int>
        if (this.wpTextbox1 == null) {
            // This is not an "?action=edit" page
            return;
        }
        this.root = $elem("DIV"); // : Element; that's where suggestions are rendered
        var e = editform;
        while ((e.previousSibling != null) && (e.previousSibling.id != "toolbar")) {
            e = e.previousSibling;
        }
        e.parentNode.insertBefore(this.root, e);
        this.root2 = $elem("DIV"); // : Element; the proposed edit summary appears there
        wpSummaryLabel.parentNode.insertBefore(this.root2, wpSummaryLabel);
        this.root2.hide();
        this.scan();
        this.root.style.border = "dashed #ccc 1px";
        this.root2.style.border = "dashed #ccc 1px";
        this.root.style.color = "#888";
        this.root2.style.color = "#888";
    },

    scan: function () {
        var s = this.wpTextbox1.value;
        this.suggestions = [];
        for (var i = 0; i < AdvisorRules.RULES.length; i++) {
            var a = AdvisorRules.RULES[i](s);
            if (a.constructor == AdvisorSuggestion) {
                a = [ a ];
            }
            for (var j = 0; j < a.length; j++) {
                this.suggestions.push(a[j]);
            }
        }
        this.suggestions.sort(function (x, y) {
            return (x.start < y.start) ? -1 :
                   (x.start > y.start) ? 1 :
                   (x.end < y.end) ? -1 :
                   (x.end > y.end) ? 1 : 0;
        });
        this.root.removeAllChildren();
        if (this.suggestions.length == 0) {
            this.root.appendText(_.INFO_NOSUGGESTIONS);
        } else {
            var nSuggestions = Math.min(this.maxSuggestions, this.suggestions.length);
            this.root.appendText(
                    (this.suggestions.length == 1)
                            ? _.LABEL_ONESUGGESTION
                            : (this.suggestions.length + _.LABEL_SUGGESTIONS)
            );
            for (var i = 0; i < nSuggestions; i++) {
                this.root.appendChildren(
                        $anchor(
                                this.suggestions[i].name,
                                this.markSuggestion.bind(this, this.suggestions[i]),
                                this.suggestions[i].description
                        ),
                        $elem("SUP", $anchor(_.BUTTON_FIX, this.fixSuggestion.bind(this, this.suggestions[i]))),
                        " "
                );
            }
            if (this.suggestions.length > this.maxSuggestions) {
                this.root.appendChildren(
                        $anchor(
                                "...",
                                function () {
                                    this.maxSuggestions = 1000;
                                    this.scan();
                                    this.maxSuggestions = AdvisorPlugin.DEFAULT_MAX_SUGGESTIONS;
                                    return false;
                                }.bind(this),
                                _.BUTTON_SHOWALL
                        ), " "
                );
            }
        }
        this.root.appendChildren("(", $anchor(_.BUTTON_RESCAN, this.scan.bind(this), _.INFO_REPEATANALYSIS), ")");
    },

    fixSuggestion: function (suggestion) {
        if (this.wpTextbox1.value.substring(suggestion.start, suggestion.end)
                        != suggestion.originalText) {
            alert(_.ERROR_APPLICABLETEXTCHANGED);
            this.scan();
            return;
        }
        this.wpTextbox1.value = this.wpTextbox1.value.substring(0, suggestion.start)
                 + suggestion.replacementText
                 + this.wpTextbox1.value.substring(suggestion.end);
        this.wpTextbox1.selectionStart = suggestion.start;
        this.wpTextbox1.selectionEnd = suggestion.start + suggestion.replacementText.length;
        this.wpTextbox1.scrollToSelection();
        // Propose an edit summary
        if (this.appliedSuggestions[suggestion.name] == null) {
            this.appliedSuggestions[suggestion.name] = 1;
        } else {
            this.appliedSuggestions[suggestion.name]++;
        }
        var a = [];
        for (var i in this.appliedSuggestions) {
            a.push(i);
        }
        a.sort(function (x, y) {
            var u = this.appliedSuggestions;
            return (u[x] > u[y]) ? -1 : (u[x] < u[y]) ? 1 : (x < y) ? -1 : (x > y) ? 1 : 0;
        }.bind(this));
        var s = "";
        for (var i = 0; i < a.length; i++) {
            var count = this.appliedSuggestions[a[i]];
            s += ", " + ((count == 1) ? a[i] : (count + "x " + a[i]));
        }
        // Cut off the leading ", " and add "formatting: " and "using Advisor.js"
        s = "formatting: " + s.substring(2) + " (using [[User:Cameltrader#Advisor.js|Advisor.js]])";
        // Render in DOM
        this.root2.removeAllChildren().appendChildren(
                $anchor(_.LABEL_ADDTOSUMMARY, this.addToSummary.bind(this, s), _.LABEL_CLICKTOFILLSUMMARY),
                ": \"" + s + "\""
        );
        this.root2.show();
        // Re-scan
        this.scan();
    },

    addToSummary: function (summary) {
        var wpSummary = this.wpSummary;
        if (wpSummary.value != "") {
            summary = wpSummary.value + "; " + summary;
        }
        if ((wpSummary.maxLength > 0) && (summary.length > wpSummary.maxLength)) {
            alert(_.ERROR_EDITSUMMARYLIMIT
                    .replace(/\{0\}/g, wpSummary.maxLength)
                    .replace(/\{1\}/g, summary.length - wpSummary.maxLength)
            );
            return;
        }
        wpSummary.value = summary;
        this.root2.hide();
    },

    markSuggestion: function (suggestion) {
        if (this.wpTextbox1.value.substring(suggestion.start, suggestion.end)
                    != suggestion.originalText) {
            // The text has changed - just do another scan and don't change selection
            this.scan();
            return;
        }
        this.wpTextbox1.selectionStart = suggestion.start;
        this.wpTextbox1.selectionEnd = suggestion.end;
        this.wpTextbox1.scrollToSelection();
    }
};
//</nowiki></pre></div></div>
<!-- --><div class="NavFrame" style="text-align: left; background-color: #dde;"><div class="NavHead" style="display: inline; padding-right: 4em; background-color: transparent;">AdvisorRules.js</div><div class="NavContent" style="text-align: left"><pre><nowiki>

var AdvisorRules = {};

AdvisorRules.RULES = []; // : Function[]

AdvisorRules.LANGUAGE_MAP = { // : Hashtable<String, String>
    // From http://wiki.riteme.site/wiki/List_of_ISO_639-1_codes
    // Note, that not all of these have a lang-xx template, but finding a reference
    // to such a language is a good reason to create the template.
    aa: "Afar", ab: "Abkhazian", ae: "Avestan", af: "Afrikaans", ak: "Akan",
    am: "Amharic", an: "Aragonese", ar: "Arabic", as: "Assamese", av: "Avaric",
    ay: "Aymara", az: "Azerbaijani", ba: "Bashkir", be: "Belarusian",
    bg: "Bulgarian", bh: "Bihari", bi: "Bislama", bm: "Bambara", bn: "Bengali",
    bo: "Tibetan", br: "Breton", bs: "Bosnian", ca: "Catalan", ce: "Chechen",
    ch: "Chamorro", co: "Corsican", cr: "Cree", cs: "Czech",
    cu: "Church Slavic", cv: "Chuvash", cy: "Welsh", da: "Danish", de: "German",
    dv: "Divehi", dz: "Dzongkha", ee: "Ewe", el: "Greek", en: "English",
    eo: "Esperanto", es: "Spanish", et: "Estonian", eu: "Basque", fa: "Persian",
    ff: "Fulah", fi: "Finnish", fj: "Fijian", fo: "Faroese", fr: "French",
    fy: "Western Frisian", ga: "Irish", gd: "Gaelic", gl: "Galician",
    gn: "Guaraní", gu: "Gujarati", gv: "Manx", ha: "Hausa", he: "Hebrew",
    hi: "Hindi", ho: "Hiri Motu", hr: "Croatian", ht: "Haitian",
    hu: "Hungarian", hy: "Armenian", hz: "Herero",
    ia: "Interlingua (International Auxiliary Language Association)",
    id: "Indonesian", ie: "Interlingue", ig: "Igbo", ii: "Sichuan Yi",
    ik: "Inupiaq", io: "Ido", is: "Icelandic", it: "Italian", iu: "Inuktitut",
    ja: "Japanese", jv: "Javanese", ka: "Georgian", kg: "Kongo", ki: "Kikuyu",
    kj: "Kuanyama", kk: "Kazakh", kl: "Kalaallisut", km: "Khmer", kn: "Kannada",
    ko: "Korean", kr: "Kanuri", ks: "Kashmiri", ku: "Kurdish", kv: "Komi",
    kw: "Cornish", ky: "Kirghiz", la: "Latin", lb: "Luxembourgish", lg: "Ganda",
    li: "Limburgish", ln: "Lingala", lo: "Lao", lt: "Lithuanian",
    lu: "Luba-Katanga", lv: "Latvian", mg: "Malagasy", mh: "Marshallese",
    mi: "Māori", mk: "Macedonian", ml: "Malayalam", mn: "Mongolian",
    mo: "Moldavian", mr: "Marathi", ms: "Malay", mt: "Maltese", my: "Burmese",
    na: "Nauru", nb: "Norwegian Bokmål", nd: "North Ndebele", ne: "Nepali",
    ng: "Ndonga", nl: "Dutch", nn: "Norwegian Nynorsk", no: "Norwegian",
    nr: "South Ndebele", nv: "Navajo", ny: "Chichewa", oc: "Occitan",
    oj: "Ojibwa", om: "Oromo", or: "Oriya", os: "Ossetian", pa: "Panjabi",
    pi: "Pāli", pl: "Polish", ps: "Pashto", pt: "Portuguese", qu: "Quechua",
    rm: "Raeto-Romance", rn: "Kirundi", ro: "Romanian", ru: "Russian",
    rw: "Kinyarwanda", sa: "Sanskrit", sc: "Sardinian", sd: "Sindhi",
    se: "Northern Sami", sg: "Sango", sh: "Serbo-Croatian", si: "Sinhala",
    sk: "Slovak", sl: "Slovenian", sm: "Samoan", sn: "Shona", so: "Somali",
    sq: "Albanian", sr: "Serbian", ss: "Swati", st: "Southern Sotho",
    su: "Sundanese", sv: "Swedish", sw: "Swahili", ta: "Tamil", te: "Telugu",
    tg: "Tajik", th: "Thai", ti: "Tigrinya", tk: "Turkmen", tl: "Tagalog",
    tn: "Tswana", to: "Tonga", tr: "Turkish", ts: "Tsonga", tt: "Tatar",
    tw: "Twi", ty: "Tahitian", ug: "Uighur", uk: "Ukrainian", ur: "Urdu",
    uz: "Uzbek", ve: "Venda", vi: "Vietnamese", vo: "Volapük", wa: "Walloon",
    wo: "Wolof", xh: "Xhosa", yi: "Yiddish", yo: "Yoruba", za: "Zhuang",
    zh: "Chinese", zu: "Zulu"
};

AdvisorRules.REVERSE_LANGUAGE_MAP = {}; // : Hashtable<String, String>
for (var i in AdvisorRules.LANGUAGE_MAP) {
    AdvisorRules.REVERSE_LANGUAGE_MAP[AdvisorRules.LANGUAGE_MAP[i]] = i;
}

AdvisorRules.MONTH_MAP = {
    Jan: "January", Feb: "February", Mar: "March", Apr: "April", May: "May",
    Jun: "June", Jul: "July", Aug: "August", Sep: "September", Oct: "October",
    Nov: "November", Dec: "December", January: "January", February: "February",
    March: "March", April: "April", June: "June", July: "July",
    August: "August", September: "September", October: "October",
    November: "November", December: "December"
};



/*
    Rules
*/

AdvisorRules.RULES.push(function (s) {
    var re = /^[ ':]*(?:(?:Further|More) +info(?:rmation))[ ']*:[ ']*([^\]]+)[ ']*$/mig;
    var a = re.getAllMatches(s);
    var b = [];
    for (var i = 0; i < a.length; i++) {
        var m = a[i];
        if ((m[1] != null) && (m[1] != "")) {
            b.push(new AdvisorSuggestion(
                    m.start, m.end, m[0],
                    "{{futher|" + m[2] + "}}",
                    "template-further",
                    "Better use the {{futher|...}} template."
            ));
        }
    }
    return b;
});

AdvisorRules.RULES.push(function (s) {
    var re = /^[ ':]*(?:Main +article)[ ']*:[ ']*\[\[([^\]]+)\]\][ ']*$/mig;
    var a = re.getAllMatches(s);
    var b = [];
    for (var i = 0; i < a.length; i++) {
        var m = a[i];
        if ((m[1] != null) && (m[1] != "")) {
            b.push(new AdvisorSuggestion(
                    m.start, m.end, m[0],
                    "{{main|" + m[1] + "}}",
                    "template-main",
                    "Better use the {{main|...}} template."
            ));
        }
    }
    return b;
});

AdvisorRules.RULES.push(function (s) {
    // This will match either a date+year or just a year,
    // and will not match solitary dates.
    // The rule only controls the transition from linked to unlinked,
    // as practice has shown that improper linking is significantly
    // more common than leaving linkable dates as plain text.
    var re = /(?:\[\[((?:(\d\d?) +({month}))|(?:({month}) +(\d\d?)))\]\],?( )? *)?\[\[([12]\d\d\d)\]\]/;
    var a = re.fix().getAllMatches(s);
    var b = [];
    for (var i = 0; i < a.length; i++) {
        var m = a[i];
        var date  = ((m[1] == null) || (m[1] == "")) ? null : m[1];
        var year  = ((m[7] == null) || (m[7] == "")) ? null : m[7];
        if (date == null) {
            b.push(new AdvisorSuggestion(
                    m.start, m.end, m[0],
                    year,
                    "year link",
                    "It is useless to link a year unless it is preceded by a day and month."
            ));
        } else {
            var isAmerican = ((m[2] == null) || (m[2] == ""));
            var day = (isAmerican) ? m[5] : m[2];
            var month = AdvisorRules.MONTH_MAP[(isAmerican) ? m[4] : m[3]];
            var ws = m[6]; // whitespace between date and year
            var replacement = (isAmerican)
                    ? ("[[" + month + " " + day + "]]," + ws + "[[" + year + "]]")
                    : ("[[" + day + " " + month + "]]" + ws + "[[" + year + "]]");
            if (replacement != m[0]) {
                b.push(new AdvisorSuggestion(
                        m.start, m.end, m[0],
                        replacement,
                        "date",
                        "Date conventions."
                ));
            }
        }
    }
    return b;
});

AdvisorRules.RULES.push(function (s) {
    var re = /\[\[(\w+) language\|\1\]\] *: (\'+)*([{letter} \"\'\„\“\/—–\-]+)(?:\2)/g;
    var a = re.fix().getAllMatches(s);
    var b = [];
    for (var i = 0; i < a.length; i++) {
        var m = a[i];
        var code = AdvisorRules.REVERSE_LANGUAGE_MAP[m[1]] || null;
        if (code == null) {
            continue;
        }
        // Markers for italics and bold are stripped off
        b.push(new AdvisorSuggestion(
                m.start, m.end, m[0],
                "{{lang-" + code + "|" + m[3] + "}}",
                "lang-" + code,
                "The {{lang-" + code + "}} template can be applied for this text."
        ));
    }
    return b;
});

AdvisorRules.RULES.push(function (s) {
    // Only accept years 1000...2999
    var re = /[^0-9]([1-2][0-9]{3}) *(?:-|—|&mdash;|--) *([1-2][0-9]{3})[^0-9]/g;
    var a = re.getAllMatches(s);
    for (var i = 0; i < a.length; i++) {
        var m = a[i];
        a[i] = new AdvisorSuggestion(
                m.start + 1, m.end - 1, m[0].substring(1, m[0].length - 1),
                m[1] + "–" + m[2], // the char in the middle is an ndash
                "ndash",
                "Year ranges look better with an n-dash."
        );
    }
    return a;
});

AdvisorRules.RULES.push(function (s) {
    var re = /[{letter}]( +- +)[{letter}]/g;
    var a = re.fix().getAllMatches(s);
    for (var i = 0; i < a.length; i++) {
        var m = a[i];
        a[i] = new AdvisorSuggestion(
                m.start + 1, m.end - 1, m[1],
                " — ", // mdash
                "mdash",
                "In a sentence, a hyphen surrounded by spaces means almost certainly an mdash."
        );
    }
    return a;
});

AdvisorRules.RULES.push(function (s) {
    var re = /^(?: *)(==+)( *)([^=]*[^= ])(?: *)\1/gm;
    var a = re.getAllMatches(s);
    var b = [];
    var level = 0; // == Level 1 ==, === Level 2 ===, ==== Level 3 ====, etc.
    /* If we are editing a section, we have to be tolerant to the first heading's level */
    var isSection = $("editform")
                && ($("editform")["wpSection"] != null)
                && ($("editform")["wpSection"].value != "");
    for (var i = 0; i < a.length; i++) {
        var m = a[i];
        var suggestion = new AdvisorSuggestion(
                m.start, m.end, m[0],
                m[1] + m[2] + m[3] + m[2] + m[1],
                "heading",
                "Heading style should be \"== Heading ==\" or \"==Heading==\"."
        );
        var oldLevel = level;
        level = m[1].length - 1;
        if ( (level - oldLevel > 1) && (!isSection || (oldLevel > 0)) ) {
            // Adjust suggestion for the rare case of improper heading hesting
            suggestion.name = "heading-nesting";
            suggestion.description = "Improper heading nesting";
            var h = "=======".substring(0, oldLevel + 2);
            suggestion.replacementText = h + m[2] + m[3] + m[2] + h;
            b.push(suggestion);
            continue;
        }
        var frequentMistakes = [
            { code: "see-also",  wrong: /^see *al+so$/i,          correct: "See also" },
            { code: "ext-links", wrong: /^external links?$/i,     correct: "External links" },
            { code: "refs",      wrong: /^ref+e?r+en(c|s)es?$/i,  correct: "References" }
        ];
        for (var j = 0; j < frequentMistakes.length; j++) {
            var fm = frequentMistakes[j];
            if (fm.wrong.test(m[3]) && (m[3] != fm.correct)) {
                suggestion.replacementText = m[1] + m[2] + fm.correct + m[2] + m[1];
                suggestion.name = fm.code;
                suggestion.description = "The correct spelling/capitalization is \""
                        + fm.correct + "\".";
            }
        }
        if (suggestion.originalText != suggestion.replacementText) {
            b.push(suggestion);
        }
    }
    return b;
});

AdvisorRules.RULES.push(function (s) {
    var re = /\[\[([{letter} ,\(\)\-]+)\|\1\]\]/g;
    var a = re.fix().getAllMatches(s);
    for (var i = 0; i < a.length; i++) {
        var m = a[i];
        a[i] = new AdvisorSuggestion(
                m.start, m.end, m[0],
                "[[" + m[1] + "]]",
                "A|A",
                "\"[[A|A]]\" can be simplified to [[A]]."
        );
    }
    return a;
});

AdvisorRules.RULES.push(function (s) {
    var re = /\[\[([{letter} ,\(\)\-]+)\|\1([{letter}]+)\]\]/g;
    var a = re.fix().getAllMatches(s);
    for (var i = 0; i < a.length; i++) {
        var m = a[i];
        a[i] = new AdvisorSuggestion(
                m.start, m.end, m[0],
                "[[" + m[1] + "]]" + m[2],
                "A|AB",
                "\"[[A|AB]]\" can be simplified to [[A]]B."
        );
    }
    return a;
});

// This rule has to be removed. The text could be in another
// language using the Cyrillic alphabet.
/*
AdvisorRules.RULES.push(function (s) {
    // For this rule to apply, at least four of the characters within the
    // brackets must be Bulgarian Cyrillic letters, and the rest are allowed
    // to be some limited punctuation
    var re = /\((([ ,.;:\"\'\„\“\-\—\–]|&ndash;|&mdash;|&amp;|&#768;)*({letter_bg}+([ ,.;:\"\'\„\“\-\—\–]|&ndash;|&mdash;|&amp;|&#768;)*){4,})\)/g;
    var a = re.fix().getAllMatches(s); // the previous line must be monolithic because of MSIE
    for (var i = 0; i < a.length; i++) {
        var m = a[i];
        a[i] = new AdvisorSuggestion(
                m.start, m.end, m[0],
                "({{lang|bg|" + m[1] + "}})",
                "lang|bg",
                "Bulgarian text should be marked with \"{{lang|bg|}}\", "
                        + "even when there is no [[Bulgarian language]] link."
        );
    }
    return a;
});
*/

// With the introduction of the magic word "DEFAULTSORT" it became the
// preferred means to do category sorting.
/*
AdvisorRules.RULES.push(function (s) {
    var exceptions = {}; // sure, there are many
    exceptions["Dolni Bogrov"] = true;
    exceptions["Gorni Bogrov"] = true;
    if (exceptions[window.wgTitle]) {
        return;
    }
    var re0 = /^([{letter}\-]+(?: [{letter}\-]+\.?)?) ([{letter}\-]+(?:ov|ev|ski))$/;
    var m0 = re0.exec(window.wgTitle);
    if (m0 == null) {
        return [];
    }
    var firstNames = m0[1];
    var lastName = m0[2];
    // Should be "lastName, firstName" in categories
    var re1 = /\[\[(Category:[\w _\(\),\-]+)\]\]/gi;
    var a = re1.getAllMatches(s);
    for (var i = 0; i < a.length; i++) {
        var m = a[i];
        a[i] = new AdvisorSuggestion(
                m.start, m.end, m[0],
                "[[" + m[1] + "|" + lastName + ", " + firstNames + "]]",
                "cat-sort",
                "The sort key for categories should be the family name."
                // Wikipedia:Categorization_of_people#Ordering_names_in_a_category
        );
    }
    return a;
});
*/

AdvisorRules.RULES.push(function (s) {
    var exceptions = {};
    exceptions["Dolni Bogrov"] = true;
    exceptions["Gorni Bogrov"] = true;
    if (exceptions[window.wgTitle]) {
        return [];
    }
    var re0 = /^([{letter}\-]+(?: [{letter}\-]+\.?)?) ([{letter}\-]+(?:ov|ev|ski))$/;
    var m0 = re0.fix().exec(window.wgTitle);
    if (m0 == null) {
        return [];
    }
    if (s.indexOf("DEFAULTSORT") != -1) {
        return [];
    }
    var firstNames = m0[1];
    var lastName = m0[2];
    /*var re1 = /\[\[(Category:[\w _\(\),\-]+)\|([\w _\(\),\-]+)\]\]/gi;*/
    var re1 = new RegExp("\\[\\[(Category:[\\w _\\(\\),\\-]+)\\| *"
                        + RegExp.escape(lastName) + ", *" + RegExp.escape(firstNames)
                        + " *\\]\\]", "gi");
    var a = re1.getAllMatches(s);
    if (a.length == 0) {
        return [];
    }
    var aStart = a[0].start;
    var aEnd = a[a.length - 1].end;
    var original = s.substring(aStart, aEnd);
    var replacement = "{{DEFAULTSORT:" + lastName + ", " + firstNames + "}}\n"
                    + original.replace(re1, "[[$1]]");
    return [
        new AdvisorSuggestion(
                aStart, aEnd, original, replacement, "default-sort",
                "The magic word DEFAULTSORT can be used to specify sort keys for categories."
        )
    ];
});

AdvisorRules.RULES.push(function (s) {
    var re = /\( *(?:b\.? *)?((?:19|20)[0-9][0-9]) *(?:[\-\—\–]|&ndash;|&mdash;|--) *\)/g;
    var a = re.getAllMatches(s);
    for (var i = 0; i < a.length; i++) {
        var m = a[i];
        a[i] = new AdvisorSuggestion(
                m.start, m.end, m[0],
                "(born " + m[1] + ")",
                "born",
                "The word \"born\" should be fully written."
                // WP:DATE#Dates_of_birth_and_death
        );
    }
    return a;
});
//</nowiki></pre></div></div>
<!-- --><div class="NavFrame" style="text-align: left; background-color: #dde;"><div class="NavHead" style="display: inline; padding-right: 4em; background-color: transparent;">AdvisorSuggestion.js</div><div class="NavContent" style="text-align: left"><pre><nowiki>
/*
    It makes sense to have a designated class for suggestions, and not use bare { name: value } syntax.
    Thus we have a clear concept of a suggestion, and we can validate parameters in the constructor.
*/

var AdvisorSuggestion = Class.create();

AdvisorSuggestion.prototype = {
    initialize: function (start, end, originalText, replacementText, name, description) {
        // todo: validation
        this.start = start;
        this.end = end;
        this.originalText = originalText;
        this.replacementText = replacementText;
        this.name = name;
        this.description = description;
    }
};
//</nowiki></pre></div></div>
<!-- --><div class="NavFrame" style="text-align: left; background-color: #dde;"><div class="NavHead" style="display: inline; padding-right: 4em; background-color: transparent;">AdvisorHighlighterPlugin.js</div><div class="NavContent" style="text-align: left"><pre><nowiki>

var AdvisorHighlighterPlugin = Class.create();

AdvisorHighlighterPlugin.prototype = {

    initialize: function (advisorPlugin) {
        if (advisorPlugin == null) {
            throw new Exception("An instance of AdvisorPlugin must be passed to the constructor of AdvisorHighlighterPlugin.");
        }
        this.advisorPlugin = advisorPlugin;
        if (advisorPlugin.wpTextbox1 == null) {
            // nothing to do, this is not an "?action=edit" page
            return;
        }
        this.overlays = [ advisorPlugin.wpTextbox1 ];
        this.addOverlay("yellow");
        this.addOverlay("pink");
        Event.observe(this.overlays[0], "keyup", this.processInputEvent.bind(this));
        Event.observe(this.overlays[0], "mouseover", this.processInputEvent.bind(this));
    },

    addOverlay: function (colour) {
        var overlay = this.overlays[0].cloneNode(true);
        var previousOverlay = this.overlays.last();
        overlay.id += "overlay" + this.overlays.length;
        overlay.readOnly = true;
        overlay.style.color = colour;
        overlay.style.position = "";
        previousOverlay.style.position = "absolute";
//        previousOverlay.style.width = overlay.clientWidth + "px";
        previousOverlay.style.zIndex = 1000 - this.overlays.length;
        previousOverlay.parentNode.insertBefore(overlay, previousOverlay.nextSibling);
        previousOverlay.style.backgroundColor = "transparent";
        this.overlays.push(overlay);
    },

    processInputEvent: function (event) {
        var a = this.overlays[0];
        if (a.value == a.oldValue) {
            var scrollTop = a.scrollTop;
            if (a.oldScrollTop != scrollTop) {
                a.oldScrollTop = scrollTop;
                for (var i = 0; i < this.overlays.length; i++) {
                    this.overlays[i].scrollTop = scrollTop;
                }
            }
            var scrollLeft = a.scrollLeft;
            if (a.oldScrollLeft != scrollLeft) {
                a.oldScrollLeft = scrollLeft;
                for (var i = 0; i < this.overlays.length; i++) {
                    this.overlays[i].scrollLeft = scrollLeft;
                }
            }
            return;
        }
        a.oldValue = a.value;
        this.updateHighlighting.bind(this).delay(1000);
    },

    updateHighlighting: function () {
        this.advisorPlugin.scan();
        var suggestions = this.advisorPlugin.suggestions;
        var s = this.overlays[0].value.replace(/[^ \n\r\t]/g, "\u00a0"); // U+00a0 is a non-breaking space
        var values = [ "" ];
        for (var i = 1; i < this.overlays.length; i++) {
            values[i] = s;
        }
        for (var i = 0; i < suggestions.length; i++) {
            var x = suggestions[i];
            var overlayIndex = 2; // todo
            var v = values[overlayIndex];
            values[overlayIndex] = v.substring(0, x.start)
                    + v.substring(x.start, x.end)
                            .replace(/\u00a0/g, "\u2588") // U+2588 is a full solid block
                    + v.substring(x.end);
        }
        for (var i = 1; i < this.overlays.length; i++) {
            this.overlays[i].value = values[i];
            this.overlays[i].scrollTop = this.overlays[0].scrollTop;
            this.overlays[i].scrollLeft = this.overlays[0].scrollLeft;
        }
    }
};

// A candidate for FunctionExtensions.js
Object.extend(Function.prototype, {
    delay: function (t) {
        var a = $A(arguments);
        a.splice(1);
        clearTimeout(this.timeoutId);
        this.timeoutId = setTimeout(this.bind.apply(this, a), t);
    }
});
//</nowiki></pre></div></div>
<!-- --><div class="NavFrame" style="text-align: left; background-color: #dde;"><div class="NavHead" style="display: inline; padding-right: 4em; background-color: transparent;">ContactListPlugin.js</div><div class="NavContent" style="text-align: left"><pre><nowiki>

var ContactListPlugin = Class.create();

ContactListPlugin.prototype = {

    initialize: function () {
        var eH5 = $elem("H5").appendChildren(
                _.LABEL_CONTACTS + " (",
                $anchor(_.BUTTON_REFRESH, this.refresh.bind(this)), ", ",
                $anchor(_.BUTTON_EDIT, this.edit.bind(this)), ")"
        );
        var ePBody = $elem("DIV").addClassName("pBody");
        ePBody.id = "contact-list-body";
        $("column-one").appendChild($elem("DIV", eH5, ePBody).addClassName("portlet"));
        this.refresh();
    },

    refresh: function () {
        $("contact-list-body").removeAllChildren();
        var contacts = CookieManager.getCookie("contacts");
        if ((contacts == null) || (contacts == "")) {
            $("contact-list-body").appendText(_.LABEL_EMPTY);
            return;
        }
        $("contact-list-body").appendText(_.INFO_REFRESHING);
        new Ajax.Request("/w/query.php", { // unfortunately, "/w/api.php" doesn't offer retrieval of user contributions yet
            method: "get",
            parameters: {
                format: "json",
                what: "usercontribs",
                uclimit: 20,
                uccomments: "",
                titles: "User:" + contacts.split("|").join("|User:")
            },
            onSuccess: function (transport) {
                var result = new Function("return " + transport.responseText)();
                var a = [];
                for (var i in result.pages) {
                    var p = result.pages[i];
                    a[a.length] = {
                            user: p.title,
                            timestamp: this.parseWPDate(p.contributions[0].timestamp)
                    };
                }
                // Order by last edit
                a.sort(function (x, y) {
                    return y.timestamp.getTime() - x.timestamp.getTime();
                });
                var eUL = $elem("UL");
                for (var i = 0; i < a.length; i++) {
                    var u = a[i].user;
                    if (u.substring(0, 5) == "User:") {
                        u = u.substring(5);
                    }
                    eUL.appendChild($elem("LI",
                            $anchor(u, "/wiki/Special:Contributions/" + escape(u)),
                            " " + this.getHumanReadableTimestamp(a[i].timestamp).replace(/ /g, "\u00a0") // nbsp
                    ));
                }
                $("contact-list-body").removeAllChildren().appendChild(eUL);
            }.bind(this)
        });
    },

    parseWPDate: function (s) {
        // "s" is like "2007-04-15T11:06:53Z"
        return new Date(Date.UTC(
                parseInt(s.substring(0, 4)),
                parseInt(s.substring(5, 7) - 1), // months are zero based in js
                parseInt(s.substring(8, 10)),
                parseInt(s.substring(11, 13)),
                parseInt(s.substring(14, 16)),
                parseInt(s.substring(17, 19))
        ));
    },

    getHumanReadableTimestamp: function (dateThen) {
        // todo: i18n
        var dateNow = new Date();
        // Calculate difference between now and then
        var diff = dateNow.getTime() - dateThen.getTime();
        if (diff < 2 * 60 * 1000) { // less than a two minutes
            return "just now";
        } else if (diff < 59 * 60 * 1000) { // minutes ago
            return Math.round(diff / (60 * 1000)) + "m ago";
        } else if (diff < 4 * 60 * 60 * 1000) { // hours and minutes ago
            var h = Math.floor(diff / (60 * 60 * 1000));
            var m = Math.round((diff - (h * 60 * 60 * 1000)) / (5 * 60 * 1000)) * 5; // round at 5m
            return (m == 0) ? (h + "h ago") : (h + "h " + m + "m ago");
        }
        // Calculate difference between last midnight (browser's timezone) and then
        dateNow.setHours(0);
        dateNow.setMinutes(0);
        dateNow.setSeconds(0);
        dateNow.setMilliseconds(0);
        var diffDays = Math.floor((dateNow.getTime() - dateThen.getTime()) / (24 * 60 * 60 * 1000));
        if (diffDays < 1) {
            return Math.floor(diff / (60 * 60 * 1000)) + "h ago";
        } else if (diffDays == 1) {
            return "yesterday";
        } else {
            return diffDays + "d ago";
        }
    },

    // What follows are methods related to the "edit" dialog

    edit: function () {
        this.eSettingsInnerDiv = $elem("DIV");
        this.eSettingsOuterDiv = $elem("DIV",
                this.eSettingsInnerDiv,
                $anchor(_.BUTTON_CLOSE, this.closeEditDialog.bind(this))
        );
        this.renderEditDialog();
        this.isModified = false;
        ModalDialogManager.show(this.eSettingsOuterDiv);
    },

    closeEditDialog: function () {
        ModalDialogManager.hide();
        if (this.isModified) {
            this.refresh();
            this.isModified = false;
        }
    },

    addContact: function (s) {
        if (s == "") {
            return;
        }
        var contactsCookie = CookieManager.getCookie("contacts");
        var contacts = ((contactsCookie == null) || (contactsCookie == ""))
                    ? [] : contactsCookie.split("|");
        contacts.sort();
        contacts.push(s);
        CookieManager.setCookie("contacts", contacts.join("|"));
        this.isModified = true;
        this.renderEditDialog();
    },

    addContactFromInputField: function () {
        this.addContact(this.eInput.value);
    },

    removeContact: function (index) {
        var contactsCookie = CookieManager.getCookie("contacts");
        var contacts = ((contactsCookie == null) || (contactsCookie == ""))
                    ? [] : contactsCookie.split("|");
        contacts.splice(index, 1);
        CookieManager.setCookie("contacts", contacts.join("|"));
        this.renderEditDialog();
        this.isModified = true;
    },

    renderEditDialog: function () {
        this.eSettingsInnerDiv.removeAllChildren();
        var contactsCookie = CookieManager.getCookie("contacts");
        var contacts = ((contactsCookie == null) || (contactsCookie == ""))
                    ? [] : contactsCookie.split("|");
        contacts.sort();
        this.eInput = $elem("INPUT");
        Event.observe(this.eInput, "keyup", function (event) {
            switch (event.keyCode) {
                case Event.KEY_ESC: {
                    this.closeEditDialog();
                    return false;
                }
                case Event.KEY_RETURN: {
                    this.addContactFromInputField();
                    return false;
                }
                default: {
                    // let go
                }
            }
        }.bindAsEventListener(this));
        this.eSettingsInnerDiv.appendChildren(
                this.eInput, $anchor(_.BUTTON_ADD, this.addContactFromInputField.bind(this))
        );
        if (contacts.length != 0) {
            var eUL = $elem("UL");
            for (var i = 0; i < contacts.length; i++) {
                eUL.appendChild($elem("LI",
                        $anchor(contacts[i], wgServer + "/wiki/User:" + contacts[i]),
                        " (", $anchor(_.LABEL_TALK, wgServer + "/wiki/User talk:" + contacts[i]),
                        "·", $anchor(_.LABEL_CONTRIBS, wgServer + "/wiki/Special:Contributions/" + contacts[i]),
                        ") [", $anchor(_.BUTTON_REMOVE, this.removeContact.bind(this, i)), "]"
                ));
            }
            this.eSettingsInnerDiv.appendChild(eUL);
        }
        setTimeout(this.eInput.focus.bind(this.eInput), 10);
    }
};
//</nowiki></pre></div></div>
<!-- --><div class="NavFrame" style="text-align: left; background-color: #dde;"><div class="NavHead" style="display: inline; padding-right: 4em; background-color: transparent;">ZockyLinkCompletePlugin.js</div><div class="NavContent" style="text-align: left"><pre><nowiki>
/*
    This is a modified version of User:Zocky's excellent "Link Complete" script.
        http://wiki.riteme.site/wiki/User:Zocky/Link_Complete
    I made it look a bit more object-oriented and prototype.js-based.
    Surprisingly, the thing is still working after my chaotic refactoring.
*/

var ZockyLinkCompletePlugin = Class.create();

ZockyLinkCompletePlugin.prototype = {

    initialize: function () {
        // Fields
        this.status = "idle";
        this.find = "";
        this.matches = [];
        this.nextPage = null;
        this.nextMatch = 0;
        this.start = 0;
        this.target = null;
        this.regExp = [];
        //
        var wpTextbox1 = $("wpTextbox1");
        if (wpTextbox1) {
            Event.observe(wpTextbox1, "keydown", this.keyHandler.bind(this));
            Event.observe(wpTextbox1, "keyup", this.keyIgnorer.bind(this));
            Event.observe(wpTextbox1, "keypress", this.keyIgnorer.bind(this));
            this.regExp = window.linkCompleteTriggers || [ /\[\[([^\[\]\|\n]*?)$/ ];
        }
    },

    insert: function (s) {
        var top = this.target.scrollTop;
        this.target.value =
                this.target.value.substr(0, this.start)
                + s
                + this.target.value.substr(this.selectionGetStart(this.target));
        this.target.scrollTop = top;
        this.selectionSet(this.target, this.start + s.length, this.start + s.length);
    },

    insertMatch: function () {
        if (this.nextMatch >= this.matches.length) {
            return false;
        }
        this.insert(this.matches[this.nextMatch]);
        this.nextMatch++;
        return true;
    },

    getMatches: function (from) {
        this.status = "waiting";
        new Ajax.Request(wgScriptPath + "/api.php", {
            parameters: {
                format: "json",
                action: "query",
                list: "allpages",
                apfrom: from,
                aplimit: 50,
                apfilterredir: "nonredirects",
                apnamespace: window.linkCompleteNamespace || 0
            },
            method: "get",
            onSuccess: function (transport) {
                this.ajaxHandler(transport.responseText.parseJSON());
            }.bind(this)
        });
        this.insert(this.find + "...");
    },

    reset: function () {
        this.status = "idle";
        this.insert(this.find);
        this.matches = [];
    },

    keyIgnorer: function (e) {
        var keynum = e.charCode || e.keyCode;
        if (keynum == Event.KEY_TAB) {
            Event.stop(e);
        }
    },

    keyHandler: function (e) {
        var keynum = e.charCode || e.keyCode;
        var target = Event.element(e);
        if ( ((keynum == Event.KEY_TAB) || (e.ctrlKey && (keynum == 32)))
                    && (this.selectionGetStart(target) == this.selectionGetEnd(target)) ) {
            if (target != this.target) {
                this.target = target;
                this.status = "idle";
            }

            switch (this.status) {
                case "idle": {
                    var find;
                    for (var i in this.regExp) {
                        find = (target.value.substr(0, this.selectionGetStart(target)).match(this.regExp[i]) || [])[1];
                        if (find) break;
                    }
                    if (find) {
                        this.matches = [];
                        this.nextMatch = 0;
                        this.find = find;
                        this.start = this.selectionGetStart(target) - find.length;
                        this.getMatches(find.capitalize());
                    }
                    break;
                }
                case "waiting": {
                    break;
                }
                case "loaded": {
                    if (this.nextMatch < this.matches.length) {
                        if (!this.insertMatch()) {
                            this.reset();
                        }
                    } else {
                        if (this.nextPage) {
                            this.getMatches(this.nextPage);
                        } else {
                            this.nextMatch = 0;
                            if (!this.insertMatch()) {
                                this.reset();
                            }
                        }
                    }
                    break;
                }
                default: {
                    break;
                }
            }
            Event.stop(e);
        } else {
            if (this.status == "waiting") {
                this.reset();
            }
            this.status = "idle";
        }
    },

    // JSON callback
    ajaxHandler: function (obj) {
        if (obj) {
            if (this.status == "waiting") {
                for (var i in obj.query.allpages) {
                    page = obj.query.allpages[i];
                    if (page.title
                            && (page.ns && page.title.replace(/^.*?:/, "").substr(0, this.find.length) == this.find.capitalize())
                            || (page.title.substr(0, this.find.length) == this.find.capitalize())) {
                        this.matches[this.matches.length]
                                = (page.ns) ? page.title : this.find + page.title.substr(this.find.length);
                    }
                }
                this.nextPage = (obj["query-continue"])
                        ? (
                                    (obj["query-continue"].allpages.apfrom.substr(0, this.find.length) == this.find.capitalize())
                                            ? obj["query-continue"].allpages.apfrom
                                            : false
                        )
                        : false;

                if (this.insertMatch()) {
                    this.status = "loaded";
                } else {
                    this.reset();
                }
            }
        } else {
            this.reset();
        }
    },

    selectionSet: function (input, start, end) {
        if (input.setSelectionRange) {
            input.setSelectionRange(start, end)
            input.selectionEnd = end;
        } else {
            var range = input.createTextRange();
            range.collapse(true);
            range.moveStart("character", start);
            range.moveEnd("character", end - start);
            range.select();
        }
    },

    selectionGetStart: function (input) {
        if (input.setSelectionRange) {
            return input.selectionStart;
        } else {
            var range = document.selection.createRange();
            var isCollapsed = range.compareEndPoints("StartToEnd", range) == 0;
            if (!isCollapsed) {
                range.collapse(true);
            }
            var b = range.getBookmark();
            return b.charCodeAt(2) - 2;
        }
    },

    selectionGetEnd: function (input) {
        if (input.setSelectionRange) {
            return input.selectionEnd;
        } else {
            var range = document.selection.createRange();
            var isCollapsed = range.compareEndPoints("StartToEnd", range) == 0;
            if (!isCollapsed) {
                range.collapse(false);
            }
            var b = range.getBookmark();
            return b.charCodeAt(2) - 2;
        }
    }
};

// todo: Cameltrader - decide what to do with these

//from http://www.json.org/json.js
String.prototype.parseJSON = function () {
    try {
        return !(/[^,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]/.test(
                this.replace(/"(\\.|[^"\\])*"/g, ""))) &&
               eval("(" + this + ")");
    } catch (e) {
        return false;
    }
};

//"foo".capitalize() -> "Foo"
String.prototype.capitalize = function () {
    return this.substring(0, 1).toUpperCase() + this.substring(1)
};
//</nowiki></pre></div></div>
<!-- --><div class="NavFrame" style="text-align: left; background-color: #dde;"><div class="NavHead" style="display: inline; padding-right: 4em; background-color: transparent;">CyrillicPlugin.js</div><div class="NavContent" style="text-align: left"><pre><nowiki>
/*
    Contains enhancements which are suitable for the Bulgarian-language wikipedia
*/

var CyrillicPlugin = Class.create();

CyrillicPlugin.prototype = {

    initialize: function () {
        // Precompute utf-8-to-codepoint conversion table
        var cyr = {};
        for (var ch = 0x0400; ch <= 0x052f; ch++) {
            var chUtf8 = new Number( ((ch << 2) & 0x1f00) + (ch & 0x3f) + 0xc080 ).toString(16);
            var chPercent = "%" + chUtf8.substring(0, 2) + "%" + chUtf8.substring(2);
            cyr[chPercent] = cyr[chPercent.toUpperCase()] = String.fromCharCode(ch);
        }
        // Fix anchors
        for (var i = 0; i < document.links.length; i++) {
            var href = document.links[i].href;
            var re = /%(D[01])%([0-9a-fA-F]{2})/gi;
            while (true) {
                re.lastIndex = 0;
                var m = re.exec(href);
                if (m == null) {
                    break;
                }
                href = href.substring(0, m.index) + cyr[m[0]]
                     + href.substring(m.index + m[0].length);
            }
            document.links[i].href = href;
        }
    }
};
//</nowiki></pre></div></div>
<!-- --><div class="NavFrame" style="text-align: left; background-color: #dde;"><div class="NavHead" style="display: inline; padding-right: 4em; background-color: transparent;">SubstitutionsPlugin.js</div><div class="NavContent" style="text-align: left"><pre><nowiki>

var SubstitutionsPlugin = Class.create();

SubstitutionsPlugin.SUBSTITUTIONS = [
        { charCodeSequence: "\u0448\u0448", // sh-sh
            oldText: /\u0448\u0448$/, newText: "[[" },
        { charCodeSequence: "\u0449\u0449", // sht-sht
            oldText: /\u0449\u0449$/, newText: "]]" },
        { charCodeSequence: "\u0428\u0428", // Sh-Sh
            oldText: /\u0428\u0428$/, newText: "{{" },
        { charCodeSequence: "\u0429\u0429", // Sht-Sht
            oldText: /\u0429\u0429$/, newText: "}}" },
        { charCodeSequence: "\u044e\u044e", // yu-yu
            oldText: /\u044e\u044e$/, newText: "|" },
        { charCodeSequence: "",
            oldText: /(^|[ \n-])"([{letter}])$/.fix(), newText: "$1\u201e$2" },
        { charCodeSequence: "\"",
            oldText: /([{letter}])"$/.fix(), newText: "$1\u201c" }
];

// number of letters before cursor required for replacements
// or the size of the longest charCode sequence
SubstitutionsPlugin.CONTEXT_SIZE = 20;

SubstitutionsPlugin.prototype = {

    initialize: function () {
        if ($("wpTextbox1") == null) {
            return;
        }
        this.recentCharCodes = "";
        Event.observe("wpTextbox1", "keypress", this.processKeyStroke.bind(this));
/*
        var f = function (event) {
            $("out").value = "{type=" + event.type
                    + ",keyCode=0x" + event.keyCode.toString(16)
                    + ",charCode=0x" + event.charCode.toString(16)
                    + "}\n" + $("out").value;

        }
        Event.observe("wpTextbox1", "keyup", f);
        Event.observe("wpTextbox1", "keypress", f);
        Event.observe("wpTextbox1", "keydown", f);
*/
    },

    processKeyStroke: function (event) {
        if (event.charCode == 0) {
            this.recentCharCodes = "";
            return true;
        }
        this.recentCharCodes += String.fromCharCode(event.charCode);
        if (this.recentCharCodes.length > 2 * SubstitutionsPlugin.CONTEXT_SIZE) {
            this.recentCharCodes = this.recentCharCodes.substring(SubstitutionsPlugin.CONTEXT_SIZE);
        }
        var ta = $("wpTextbox1");
        var p = ta.selectionStart;
        var s0 = ta.value.substring(p - SubstitutionsPlugin.CONTEXT_SIZE, p);
        var t = ta.scrollTop;
        for (var i = 0; i < SubstitutionsPlugin.SUBSTITUTIONS.length; i++) {
            var x = SubstitutionsPlugin.SUBSTITUTIONS[i];
            if ((x.charCodeSequence == "")
                    || (this.recentCharCodes.substr(-x.charCodeSequence.length) == x.charCodeSequence)) {
                var s1 = (s0 + String.fromCharCode(event.charCode)).replace(x.oldText, x.newText);
                if (s0 + String.fromCharCode(event.charCode) != s1) {
                    ta.value = ta.value.substring(0, p - s0.length) + s1 + ta.value.substring(p);
                    ta.selectionStart = ta.selectionEnd = p - s0.length + s1.length;
                    ta.scrollTop = t;
                    Event.stop(event);
                    this.recentCharCodes = "";
                    break;
                }
            }
        }
    }
};
//</nowiki></pre></div></div>