PageRenderTime 185ms CodeModel.GetById 117ms app.highlight 55ms RepoModel.GetById 1ms app.codeStats 1ms

/testing/selenium-core/scripts/htmlutils.js

http://datanucleus-appengine.googlecode.com/
JavaScript | 1551 lines | 1118 code | 132 blank | 301 comment | 226 complexity | bb8a530900c371528227e50d5f6c9989 MD5 | raw file
   1/*
   2 * Copyright 2004 ThoughtWorks, Inc
   3 *
   4 *  Licensed under the Apache License, Version 2.0 (the "License");
   5 *  you may not use this file except in compliance with the License.
   6 *  You may obtain a copy of the License at
   7 *
   8 *      http://www.apache.org/licenses/LICENSE-2.0
   9 *
  10 *  Unless required by applicable law or agreed to in writing, software
  11 *  distributed under the License is distributed on an "AS IS" BASIS,
  12 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 *  See the License for the specific language governing permissions and
  14 *  limitations under the License.
  15 *
  16 */
  17
  18// This script contains a badly-organised collection of miscellaneous
  19// functions that really better homes.
  20
  21function classCreate() {
  22    return function() {
  23      this.initialize.apply(this, arguments);
  24    }
  25}
  26
  27function objectExtend(destination, source) {
  28  for (var property in source) {
  29    destination[property] = source[property];
  30  }
  31  return destination;
  32}
  33
  34function sel$() {
  35  var results = [], element;
  36  for (var i = 0; i < arguments.length; i++) {
  37    element = arguments[i];
  38    if (typeof element == 'string')
  39      element = document.getElementById(element);
  40    results[results.length] = element;
  41  }
  42  return results.length < 2 ? results[0] : results;
  43}
  44
  45function sel$A(iterable) {
  46  if (!iterable) return [];
  47  if (iterable.toArray) {
  48    return iterable.toArray();
  49  } else {
  50    var results = [];
  51    for (var i = 0; i < iterable.length; i++)
  52      results.push(iterable[i]);
  53    return results;
  54  }
  55}
  56
  57function fnBind() {
  58  var args = sel$A(arguments), __method = args.shift(), object = args.shift();
  59  var retval = function() {
  60    return __method.apply(object, args.concat(sel$A(arguments)));
  61  }
  62  retval.__method = __method;
  63  return retval;
  64}
  65
  66function fnBindAsEventListener(fn, object) {
  67  var __method = fn;
  68  return function(event) {
  69    return __method.call(object, event || window.event);
  70  }
  71}
  72
  73function removeClassName(element, name) {
  74    var re = new RegExp("\\b" + name + "\\b", "g");
  75    element.className = element.className.replace(re, "");
  76}
  77
  78function addClassName(element, name) {
  79    element.className = element.className + ' ' + name;
  80}
  81
  82function elementSetStyle(element, style) {
  83    for (var name in style) {
  84      var value = style[name];
  85      if (value == null) value = "";
  86      element.style[name] = value;
  87    }
  88}
  89
  90function elementGetStyle(element, style) {
  91    var value = element.style[style];
  92    if (!value) {
  93      if (document.defaultView && document.defaultView.getComputedStyle) {
  94        var css = document.defaultView.getComputedStyle(element, null);
  95        value = css ? css.getPropertyValue(style) : null;
  96      } else if (element.currentStyle) {
  97        value = element.currentStyle[style];
  98      }
  99    }
 100
 101    /** DGF necessary? 
 102    if (window.opera && ['left', 'top', 'right', 'bottom'].include(style))
 103      if (Element.getStyle(element, 'position') == 'static') value = 'auto'; */
 104
 105    return value == 'auto' ? null : value;
 106  }
 107
 108String.prototype.trim = function() {
 109    var result = this.replace(/^\s+/g, "");
 110    // strip leading
 111    return result.replace(/\s+$/g, "");
 112    // strip trailing
 113};
 114String.prototype.lcfirst = function() {
 115    return this.charAt(0).toLowerCase() + this.substr(1);
 116};
 117String.prototype.ucfirst = function() {
 118    return this.charAt(0).toUpperCase() + this.substr(1);
 119};
 120String.prototype.startsWith = function(str) {
 121    return this.indexOf(str) == 0;
 122};
 123
 124/**
 125 * Given a string literal that would appear in an XPath, puts it in quotes and
 126 * returns it. Special consideration is given to literals who themselves
 127 * contain quotes. It's possible for a concat() expression to be returned.
 128 */
 129String.prototype.quoteForXPath = function()
 130{
 131    if (/\'/.test(this)) {
 132        if (/\"/.test(this)) {
 133            // concat scenario
 134            var pieces = [];
 135            var a = "'", b = '"', c;
 136            for (var i = 0, j = 0; i < this.length;) {
 137                if (this.charAt(i) == a) {
 138                    // encountered a quote that cannot be contained in current
 139                    // quote, so need to flip-flop quoting scheme
 140                    if (j < i) {
 141                        pieces.push(a + this.substring(j, i) + a);
 142                        j = i;
 143                    }
 144                    c = a;
 145                    a = b;
 146                    b = c;
 147                }
 148                else {
 149                    ++i;
 150                }
 151            }
 152            pieces.push(a + this.substring(j) + a);
 153            return 'concat(' + pieces.join(', ') + ')';
 154        }
 155        else {
 156            // quote with doubles
 157            return '"' + this + '"';
 158        }
 159    }
 160    // quote with singles
 161    return "'" + this + "'";
 162};
 163
 164// Returns the text in this element
 165function getText(element) {
 166    var text = "";
 167
 168    var isRecentFirefox = (browserVersion.isFirefox && browserVersion.firefoxVersion >= "1.5");
 169    if (isRecentFirefox || browserVersion.isKonqueror || browserVersion.isSafari || browserVersion.isOpera) {
 170        text = getTextContent(element);
 171    } else if (element.textContent) {
 172        text = element.textContent;
 173    } else if (element.innerText) {
 174        text = element.innerText;
 175    }
 176
 177    text = normalizeNewlines(text);
 178    text = normalizeSpaces(text);
 179
 180    return text.trim();
 181}
 182
 183function getTextContent(element, preformatted) {
 184    if (element.nodeType == 3 /*Node.TEXT_NODE*/) {
 185        var text = element.data;
 186        if (!preformatted) {
 187            text = text.replace(/\n|\r|\t/g, " ");
 188        }
 189        return text;
 190    }
 191    if (element.nodeType == 1 /*Node.ELEMENT_NODE*/) {
 192        var childrenPreformatted = preformatted || (element.tagName == "PRE");
 193        var text = "";
 194        for (var i = 0; i < element.childNodes.length; i++) {
 195            var child = element.childNodes.item(i);
 196            text += getTextContent(child, childrenPreformatted);
 197        }
 198        // Handle block elements that introduce newlines
 199        // -- From HTML spec:
 200        //<!ENTITY % block
 201        //     "P | %heading; | %list; | %preformatted; | DL | DIV | NOSCRIPT |
 202        //      BLOCKQUOTE | F:wORM | HR | TABLE | FIELDSET | ADDRESS">
 203        //
 204        // TODO: should potentially introduce multiple newlines to separate blocks
 205        if (element.tagName == "P" || element.tagName == "BR" || element.tagName == "HR" || element.tagName == "DIV") {
 206            text += "\n";
 207        }
 208        return text;
 209    }
 210    return '';
 211}
 212
 213/**
 214 * Convert all newlines to \n
 215 */
 216function normalizeNewlines(text)
 217{
 218    return text.replace(/\r\n|\r/g, "\n");
 219}
 220
 221/**
 222 * Replace multiple sequential spaces with a single space, and then convert &nbsp; to space.
 223 */
 224function normalizeSpaces(text)
 225{
 226    // IE has already done this conversion, so doing it again will remove multiple nbsp
 227    if (browserVersion.isIE)
 228    {
 229        return text;
 230    }
 231
 232    // Replace multiple spaces with a single space
 233    // TODO - this shouldn't occur inside PRE elements
 234    text = text.replace(/\ +/g, " ");
 235
 236    // Replace &nbsp; with a space
 237    var nbspPattern = new RegExp(String.fromCharCode(160), "g");
 238    if (browserVersion.isSafari) {
 239	return replaceAll(text, String.fromCharCode(160), " ");
 240    } else {
 241	return text.replace(nbspPattern, " ");
 242    }
 243}
 244
 245function replaceAll(text, oldText, newText) {
 246    while (text.indexOf(oldText) != -1) {
 247	text = text.replace(oldText, newText);
 248    }
 249    return text;
 250}
 251
 252
 253function xmlDecode(text) {
 254    text = text.replace(/&quot;/g, '"');
 255    text = text.replace(/&apos;/g, "'");
 256    text = text.replace(/&lt;/g, "<");
 257    text = text.replace(/&gt;/g, ">");
 258    text = text.replace(/&amp;/g, "&");
 259    return text;
 260}
 261
 262// Sets the text in this element
 263function setText(element, text) {
 264    if (element.textContent != null) {
 265        element.textContent = text;
 266    } else if (element.innerText != null) {
 267        element.innerText = text;
 268    }
 269}
 270
 271// Get the value of an <input> element
 272function getInputValue(inputElement) {
 273    if (inputElement.type) {
 274        if (inputElement.type.toUpperCase() == 'CHECKBOX' ||
 275            inputElement.type.toUpperCase() == 'RADIO')
 276        {
 277            return (inputElement.checked ? 'on' : 'off');
 278        }
 279    }
 280    if (inputElement.value == null) {
 281        throw new SeleniumError("This element has no value; is it really a form field?");
 282    }
 283    return inputElement.value;
 284}
 285
 286/* Fire an event in a browser-compatible manner */
 287function triggerEvent(element, eventType, canBubble, controlKeyDown, altKeyDown, shiftKeyDown, metaKeyDown) {
 288    canBubble = (typeof(canBubble) == undefined) ? true : canBubble;
 289    if (element.fireEvent && element.ownerDocument && element.ownerDocument.createEventObject) { // IE
 290        var evt = createEventObject(element, controlKeyDown, altKeyDown, shiftKeyDown, metaKeyDown);        
 291        element.fireEvent('on' + eventType, evt);
 292    }
 293    else {
 294        var evt = document.createEvent('HTMLEvents');
 295        
 296        try {
 297            evt.shiftKey = shiftKeyDown;
 298            evt.metaKey = metaKeyDown;
 299            evt.altKey = altKeyDown;
 300            evt.ctrlKey = controlKeyDown;
 301        } catch (e) {
 302            // On Firefox 1.0, you can only set these during initMouseEvent or initKeyEvent
 303            // we'll have to ignore them here
 304            LOG.exception(e);
 305        }
 306        
 307        evt.initEvent(eventType, canBubble, true);
 308        element.dispatchEvent(evt);
 309    }
 310}
 311
 312function getKeyCodeFromKeySequence(keySequence) {
 313    var match = /^\\(\d{1,3})$/.exec(keySequence);
 314    if (match != null) {
 315        return match[1];
 316    }
 317    match = /^.$/.exec(keySequence);
 318    if (match != null) {
 319        return match[0].charCodeAt(0);
 320    }
 321    // this is for backward compatibility with existing tests
 322    // 1 digit ascii codes will break however because they are used for the digit chars
 323    match = /^\d{2,3}$/.exec(keySequence);
 324    if (match != null) {
 325        return match[0];
 326    }
 327    throw new SeleniumError("invalid keySequence");
 328}
 329
 330function createEventObject(element, controlKeyDown, altKeyDown, shiftKeyDown, metaKeyDown) {
 331     var evt = element.ownerDocument.createEventObject();
 332     evt.shiftKey = shiftKeyDown;
 333     evt.metaKey = metaKeyDown;
 334     evt.altKey = altKeyDown;
 335     evt.ctrlKey = controlKeyDown;
 336     return evt;
 337}
 338
 339function triggerKeyEvent(element, eventType, keySequence, canBubble, controlKeyDown, altKeyDown, shiftKeyDown, metaKeyDown) {
 340    var keycode = getKeyCodeFromKeySequence(keySequence);
 341    canBubble = (typeof(canBubble) == undefined) ? true : canBubble;
 342    if (element.fireEvent && element.ownerDocument && element.ownerDocument.createEventObject) { // IE
 343        var keyEvent = createEventObject(element, controlKeyDown, altKeyDown, shiftKeyDown, metaKeyDown);
 344        keyEvent.keyCode = keycode;
 345        element.fireEvent('on' + eventType, keyEvent);
 346    }
 347    else {
 348        var evt;
 349        if (window.KeyEvent) {
 350            evt = document.createEvent('KeyEvents');
 351            evt.initKeyEvent(eventType, true, true, window, controlKeyDown, altKeyDown, shiftKeyDown, metaKeyDown, keycode, keycode);
 352        } else {
 353            evt = document.createEvent('UIEvents');
 354            
 355            evt.shiftKey = shiftKeyDown;
 356            evt.metaKey = metaKeyDown;
 357            evt.altKey = altKeyDown;
 358            evt.ctrlKey = controlKeyDown;
 359
 360            evt.initUIEvent(eventType, true, true, window, 1);
 361            evt.keyCode = keycode;
 362            evt.which = keycode;
 363        }
 364
 365        element.dispatchEvent(evt);
 366    }
 367}
 368
 369function removeLoadListener(element, command) {
 370    LOG.debug('Removing loadListenter for ' + element + ', ' + command);
 371    if (window.removeEventListener)
 372        element.removeEventListener("load", command, true);
 373    else if (window.detachEvent)
 374        element.detachEvent("onload", command);
 375}
 376
 377function addLoadListener(element, command) {
 378    LOG.debug('Adding loadListenter for ' + element + ', ' + command);
 379    var augmentedCommand = function() {
 380        command.call(this, element);
 381    }
 382    if (window.addEventListener && !browserVersion.isOpera)
 383        element.addEventListener("load", augmentedCommand, true);
 384    else if (window.attachEvent)
 385        element.attachEvent("onload", augmentedCommand);
 386}
 387
 388/**
 389 * Override the broken getFunctionName() method from JsUnit
 390 * This file must be loaded _after_ the jsunitCore.js
 391 */
 392function getFunctionName(aFunction) {
 393    var regexpResult = aFunction.toString().match(/function (\w*)/);
 394    if (regexpResult && regexpResult[1]) {
 395        return regexpResult[1];
 396    }
 397    return 'anonymous';
 398}
 399
 400function getDocumentBase(doc) {
 401    var bases = document.getElementsByTagName("base");
 402    if (bases && bases.length && bases[0].href) {
 403        return bases[0].href;
 404    }
 405    return "";
 406}
 407
 408function getTagName(element) {
 409    var tagName;
 410    if (element && element.tagName && element.tagName.toLowerCase) {
 411        tagName = element.tagName.toLowerCase();
 412    }
 413    return tagName;
 414}
 415
 416function selArrayToString(a) {
 417    if (isArray(a)) {
 418        // DGF copying the array, because the array-like object may be a non-modifiable nodelist
 419        var retval = [];
 420        for (var i = 0; i < a.length; i++) {
 421            var item = a[i];
 422            var replaced = new String(item).replace(/([,\\])/g, '\\$1');
 423            retval[i] = replaced;
 424        }
 425        return retval;
 426    }
 427    return new String(a);
 428}
 429
 430
 431function isArray(x) {
 432    return ((typeof x) == "object") && (x["length"] != null);
 433}
 434
 435function absolutify(url, baseUrl) {
 436    /** returns a relative url in its absolute form, given by baseUrl.
 437    * 
 438    * This function is a little odd, because it can take baseUrls that
 439    * aren't necessarily directories.  It uses the same rules as the HTML 
 440    * &lt;base&gt; tag; if the baseUrl doesn't end with "/", we'll assume
 441    * that it points to a file, and strip the filename off to find its
 442    * base directory.
 443    *
 444    * So absolutify("foo", "http://x/bar") will return "http://x/foo" (stripping off bar),
 445    * whereas absolutify("foo", "http://x/bar/") will return "http://x/bar/foo" (preserving bar).
 446    * Naturally absolutify("foo", "http://x") will return "http://x/foo", appropriately.
 447    * 
 448    * @param url the url to make absolute; if this url is already absolute, we'll just return that, unchanged
 449    * @param baseUrl the baseUrl from which we'll absolutify, following the rules above.
 450    * @return 'url' if it was already absolute, or the absolutized version of url if it was not absolute.
 451    */
 452    
 453    // DGF isn't there some library we could use for this?
 454        
 455    if (/^\w+:/.test(url)) {
 456        // it's already absolute
 457        return url;
 458    }
 459    
 460    var loc;
 461    try {
 462        loc = parseUrl(baseUrl);
 463    } catch (e) {
 464        // is it an absolute windows file path? let's play the hero in that case
 465        if (/^\w:\\/.test(baseUrl)) {
 466            baseUrl = "file:///" + baseUrl.replace(/\\/g, "/");
 467            loc = parseUrl(baseUrl);
 468        } else {
 469            throw new SeleniumError("baseUrl wasn't absolute: " + baseUrl);
 470        }
 471    }
 472    loc.search = null;
 473    loc.hash = null;
 474    
 475    // if url begins with /, then that's the whole pathname
 476    if (/^\//.test(url)) {
 477        loc.pathname = url;
 478        var result = reassembleLocation(loc);
 479        return result;
 480    }
 481    
 482    // if pathname is null, then we'll just append "/" + the url
 483    if (!loc.pathname) {
 484        loc.pathname = "/" + url;
 485        var result = reassembleLocation(loc);
 486        return result;
 487    }
 488    
 489    // if pathname ends with /, just append url
 490    if (/\/$/.test(loc.pathname)) {
 491        loc.pathname += url;
 492        var result = reassembleLocation(loc);
 493        return result;
 494    }
 495    
 496    // if we're here, then the baseUrl has a pathname, but it doesn't end with /
 497    // in that case, we replace everything after the final / with the relative url
 498    loc.pathname = loc.pathname.replace(/[^\/\\]+$/, url);
 499    var result = reassembleLocation(loc);
 500    return result;
 501    
 502}
 503
 504var URL_REGEX = /^((\w+):\/\/)(([^:]+):?([^@]+)?@)?([^\/\?:]*):?(\d+)?(\/?[^\?#]+)?\??([^#]+)?#?(.+)?/;
 505
 506function parseUrl(url) {
 507    var fields = ['url', null, 'protocol', null, 'username', 'password', 'host', 'port', 'pathname', 'search', 'hash'];
 508    var result = URL_REGEX.exec(url);
 509    if (!result) {
 510        throw new SeleniumError("Invalid URL: " + url);
 511    }
 512    var loc = new Object();
 513    for (var i = 0; i < fields.length; i++) {
 514        var field = fields[i];
 515        if (field == null) {
 516            continue;
 517        }
 518        loc[field] = result[i];
 519    }
 520    return loc;
 521}
 522
 523function reassembleLocation(loc) {
 524    if (!loc.protocol) {
 525        throw new Error("Not a valid location object: " + o2s(loc));
 526    }
 527    var protocol = loc.protocol;
 528    protocol = protocol.replace(/:$/, "");
 529    var url = protocol + "://";
 530    if (loc.username) {
 531        url += loc.username;
 532        if (loc.password) {
 533            url += ":" + loc.password;
 534        }
 535        url += "@";
 536    }
 537    if (loc.host) {
 538        url += loc.host;
 539    }
 540    
 541    if (loc.port) {
 542        url += ":" + loc.port;
 543    }
 544    
 545    if (loc.pathname) {
 546        url += loc.pathname;
 547    }
 548    
 549    if (loc.search) {
 550        url += "?" + loc.search;
 551    }
 552    if (loc.hash) {
 553        var hash = loc.hash;
 554        hash = loc.hash.replace(/^#/, "");
 555        url += "#" + hash;
 556    }
 557    return url;
 558}
 559
 560function canonicalize(url) {
 561    var tempLink = window.document.createElement("link");
 562    tempLink.href = url; // this will canonicalize the href on most browsers
 563    var loc = parseUrl(tempLink.href)
 564    if (!/\/\.\.\//.test(loc.pathname)) {
 565    	return tempLink.href;
 566    }
 567  	// didn't work... let's try it the hard way
 568  	var originalParts = loc.pathname.split("/");
 569  	var newParts = [];
 570  	newParts.push(originalParts.shift());
 571  	for (var i = 0; i < originalParts.length; i++) {
 572  		var part = originalParts[i];
 573  		if (".." == part) {
 574  			newParts.pop();
 575  			continue;
 576  		}
 577  		newParts.push(part);
 578  	}
 579  	loc.pathname = newParts.join("/");
 580    return reassembleLocation(loc);
 581}
 582
 583function extractExceptionMessage(ex) {
 584    if (ex == null) return "null exception";
 585    if (ex.message != null) return ex.message;
 586    if (ex.toString && ex.toString() != null) return ex.toString();
 587}
 588    
 589
 590function describe(object, delimiter) {
 591    var props = new Array();
 592    for (var prop in object) {
 593        try {
 594            props.push(prop + " -> " + object[prop]);
 595        } catch (e) {
 596            props.push(prop + " -> [htmlutils: ack! couldn't read this property! (Permission Denied?)]");
 597        }
 598    }
 599    return props.join(delimiter || '\n');
 600}
 601
 602var PatternMatcher = function(pattern) {
 603    this.selectStrategy(pattern);
 604};
 605PatternMatcher.prototype = {
 606
 607    selectStrategy: function(pattern) {
 608        this.pattern = pattern;
 609        var strategyName = 'glob';
 610        // by default
 611        if (/^([a-z-]+):(.*)/.test(pattern)) {
 612            var possibleNewStrategyName = RegExp.$1;
 613            var possibleNewPattern = RegExp.$2;
 614            if (PatternMatcher.strategies[possibleNewStrategyName]) {
 615                strategyName = possibleNewStrategyName;
 616                pattern = possibleNewPattern;
 617            }
 618        }
 619        var matchStrategy = PatternMatcher.strategies[strategyName];
 620        if (!matchStrategy) {
 621            throw new SeleniumError("cannot find PatternMatcher.strategies." + strategyName);
 622        }
 623        this.strategy = matchStrategy;
 624        this.matcher = new matchStrategy(pattern);
 625    },
 626
 627    matches: function(actual) {
 628        return this.matcher.matches(actual + '');
 629        // Note: appending an empty string avoids a Konqueror bug
 630    }
 631
 632};
 633
 634/**
 635 * A "static" convenience method for easy matching
 636 */
 637PatternMatcher.matches = function(pattern, actual) {
 638    return new PatternMatcher(pattern).matches(actual);
 639};
 640
 641PatternMatcher.strategies = {
 642
 643/**
 644 * Exact matching, e.g. "exact:***"
 645 */
 646    exact: function(expected) {
 647        this.expected = expected;
 648        this.matches = function(actual) {
 649            return actual == this.expected;
 650        };
 651    },
 652
 653/**
 654 * Match by regular expression, e.g. "regexp:^[0-9]+$"
 655 */
 656    regexp: function(regexpString) {
 657        this.regexp = new RegExp(regexpString);
 658        this.matches = function(actual) {
 659            return this.regexp.test(actual);
 660        };
 661    },
 662
 663    regex: function(regexpString) {
 664        this.regexp = new RegExp(regexpString);
 665        this.matches = function(actual) {
 666            return this.regexp.test(actual);
 667        };
 668    },
 669    
 670    regexpi: function(regexpString) {
 671        this.regexp = new RegExp(regexpString, "i");
 672        this.matches = function(actual) {
 673            return this.regexp.test(actual);
 674        };
 675    },
 676
 677    regexi: function(regexpString) {
 678        this.regexp = new RegExp(regexpString, "i");
 679        this.matches = function(actual) {
 680            return this.regexp.test(actual);
 681        };
 682    },
 683
 684/**
 685 * "globContains" (aka "wildmat") patterns, e.g. "glob:one,two,*",
 686 * but don't require a perfect match; instead succeed if actual
 687 * contains something that matches globString.
 688 * Making this distinction is motivated by a bug in IE6 which
 689 * leads to the browser hanging if we implement *TextPresent tests
 690 * by just matching against a regular expression beginning and
 691 * ending with ".*".  The globcontains strategy allows us to satisfy
 692 * the functional needs of the *TextPresent ops more efficiently
 693 * and so avoid running into this IE6 freeze.
 694 */
 695    globContains: function(globString) {
 696        this.regexp = new RegExp(PatternMatcher.regexpFromGlobContains(globString));
 697        this.matches = function(actual) {
 698            return this.regexp.test(actual);
 699        };
 700    },
 701
 702
 703/**
 704 * "glob" (aka "wildmat") patterns, e.g. "glob:one,two,*"
 705 */
 706    glob: function(globString) {
 707        this.regexp = new RegExp(PatternMatcher.regexpFromGlob(globString));
 708        this.matches = function(actual) {
 709            return this.regexp.test(actual);
 710        };
 711    }
 712
 713};
 714
 715PatternMatcher.convertGlobMetaCharsToRegexpMetaChars = function(glob) {
 716    var re = glob;
 717    re = re.replace(/([.^$+(){}\[\]\\|])/g, "\\$1");
 718    re = re.replace(/\?/g, "(.|[\r\n])");
 719    re = re.replace(/\*/g, "(.|[\r\n])*");
 720    return re;
 721};
 722
 723PatternMatcher.regexpFromGlobContains = function(globContains) {
 724    return PatternMatcher.convertGlobMetaCharsToRegexpMetaChars(globContains);
 725};
 726
 727PatternMatcher.regexpFromGlob = function(glob) {
 728    return "^" + PatternMatcher.convertGlobMetaCharsToRegexpMetaChars(glob) + "$";
 729};
 730
 731if (!this["Assert"]) Assert = {};
 732
 733
 734Assert.fail = function(message) {
 735    throw new AssertionFailedError(message);
 736};
 737
 738/*
 739* Assert.equals(comment?, expected, actual)
 740*/
 741Assert.equals = function() {
 742    var args = new AssertionArguments(arguments);
 743    if (args.expected === args.actual) {
 744        return;
 745    }
 746    Assert.fail(args.comment +
 747                "Expected '" + args.expected +
 748                "' but was '" + args.actual + "'");
 749};
 750
 751Assert.assertEquals = Assert.equals;
 752
 753/*
 754* Assert.matches(comment?, pattern, actual)
 755*/
 756Assert.matches = function() {
 757    var args = new AssertionArguments(arguments);
 758    if (PatternMatcher.matches(args.expected, args.actual)) {
 759        return;
 760    }
 761    Assert.fail(args.comment +
 762                "Actual value '" + args.actual +
 763                "' did not match '" + args.expected + "'");
 764}
 765
 766/*
 767* Assert.notMtches(comment?, pattern, actual)
 768*/
 769Assert.notMatches = function() {
 770    var args = new AssertionArguments(arguments);
 771    if (!PatternMatcher.matches(args.expected, args.actual)) {
 772        return;
 773    }
 774    Assert.fail(args.comment +
 775                "Actual value '" + args.actual +
 776                "' did match '" + args.expected + "'");
 777}
 778
 779
 780// Preprocess the arguments to allow for an optional comment.
 781function AssertionArguments(args) {
 782    if (args.length == 2) {
 783        this.comment = "";
 784        this.expected = args[0];
 785        this.actual = args[1];
 786    } else {
 787        this.comment = args[0] + "; ";
 788        this.expected = args[1];
 789        this.actual = args[2];
 790    }
 791}
 792
 793function AssertionFailedError(message) {
 794    this.isAssertionFailedError = true;
 795    this.isSeleniumError = true;
 796    this.message = message;
 797    this.failureMessage = message;
 798}
 799
 800function SeleniumError(message) {
 801    var error = new Error(message);
 802    if (typeof(arguments.caller) != 'undefined') { // IE, not ECMA
 803        var result = '';
 804        for (var a = arguments.caller; a != null; a = a.caller) {
 805            result += '> ' + a.callee.toString() + '\n';
 806            if (a.caller == a) {
 807                result += '*';
 808                break;
 809            }
 810        }
 811        error.stack = result;
 812    }
 813    error.isSeleniumError = true;
 814    return error;
 815}
 816
 817function highlight(element) {
 818    var highLightColor = "yellow";
 819    if (element.originalColor == undefined) { // avoid picking up highlight
 820        element.originalColor = elementGetStyle(element, "background-color");
 821    }
 822    elementSetStyle(element, {"backgroundColor" : highLightColor});
 823    window.setTimeout(function() {
 824        try {
 825            //if element is orphan, probably page of it has already gone, so ignore
 826            if (!element.parentNode) {
 827                return;
 828            }
 829            elementSetStyle(element, {"backgroundColor" : element.originalColor});
 830        } catch (e) {} // DGF unhighlighting is very dangerous and low priority
 831    }, 200);
 832}
 833
 834
 835
 836// for use from vs.2003 debugger
 837function o2s(obj) {
 838    var s = "";
 839    for (key in obj) {
 840        var line = key + "->" + obj[key];
 841        line.replace("\n", " ");
 842        s += line + "\n";
 843    }
 844    return s;
 845}
 846
 847var seenReadyStateWarning = false;
 848
 849function openSeparateApplicationWindow(url, suppressMozillaWarning) {
 850    // resize the Selenium window itself
 851    window.resizeTo(1200, 500);
 852    window.moveTo(window.screenX, 0);
 853
 854    var appWindow = window.open(url + '?start=true', 'main');
 855    if (appWindow == null) {
 856        var errorMessage = "Couldn't open app window; is the pop-up blocker enabled?"
 857        LOG.error(errorMessage);
 858        throw new Error("Couldn't open app window; is the pop-up blocker enabled?");
 859    }
 860    try {
 861        var windowHeight = 500;
 862        if (window.outerHeight) {
 863            windowHeight = window.outerHeight;
 864        } else if (document.documentElement && document.documentElement.offsetHeight) {
 865            windowHeight = document.documentElement.offsetHeight;
 866        }
 867
 868        if (window.screenLeft && !window.screenX) window.screenX = window.screenLeft;
 869        if (window.screenTop && !window.screenY) window.screenY = window.screenTop;
 870
 871        appWindow.resizeTo(1200, screen.availHeight - windowHeight - 60);
 872        appWindow.moveTo(window.screenX, window.screenY + windowHeight + 25);
 873    } catch (e) {
 874        LOG.error("Couldn't resize app window");
 875        LOG.exception(e);
 876    }
 877
 878
 879    if (!suppressMozillaWarning && window.document.readyState == null && !seenReadyStateWarning) {
 880        alert("Beware!  Mozilla bug 300992 means that we can't always reliably detect when a new page has loaded.  Install the Selenium IDE extension or the readyState extension available from selenium.openqa.org to make page load detection more reliable.");
 881        seenReadyStateWarning = true;
 882    }
 883
 884    return appWindow;
 885}
 886
 887var URLConfiguration = classCreate();
 888objectExtend(URLConfiguration.prototype, {
 889    initialize: function() {
 890    },
 891    _isQueryParameterTrue: function (name) {
 892        var parameterValue = this._getQueryParameter(name);
 893        if (parameterValue == null) return false;
 894        if (parameterValue.toLowerCase() == "true") return true;
 895        if (parameterValue.toLowerCase() == "on") return true;
 896        return false;
 897    },
 898
 899    _getQueryParameter: function(searchKey) {
 900        var str = this.queryString
 901        if (str == null) return null;
 902        var clauses = str.split('&');
 903        for (var i = 0; i < clauses.length; i++) {
 904            var keyValuePair = clauses[i].split('=', 2);
 905            var key = unescape(keyValuePair[0]);
 906            if (key == searchKey) {
 907                return unescape(keyValuePair[1]);
 908            }
 909        }
 910        return null;
 911    },
 912
 913    _extractArgs: function() {
 914        var str = SeleniumHTARunner.commandLine;
 915        if (str == null || str == "") return new Array();
 916        var matches = str.match(/(?:\"([^\"]+)\"|(?!\"([^\"]+)\")(\S+))/g);
 917        // We either want non quote stuff ([^"]+) surrounded by quotes
 918        // or we want to look-ahead, see that the next character isn't
 919        // a quoted argument, and then grab all the non-space stuff
 920        // this will return for the line: "foo" bar
 921        // the results "\"foo\"" and "bar"
 922
 923        // So, let's unquote the quoted arguments:
 924        var args = new Array;
 925        for (var i = 0; i < matches.length; i++) {
 926            args[i] = matches[i];
 927            args[i] = args[i].replace(/^"(.*)"$/, "$1");
 928        }
 929        return args;
 930    },
 931
 932    isMultiWindowMode:function() {
 933        return this._isQueryParameterTrue('multiWindow');
 934    },
 935    
 936    getBaseUrl:function() {
 937        return this._getQueryParameter('baseUrl');
 938            
 939    }
 940});
 941
 942
 943function safeScrollIntoView(element) {
 944    if (element.scrollIntoView) {
 945        element.scrollIntoView(false);
 946        return;
 947    }
 948    // TODO: work out how to scroll browsers that don't support
 949    // scrollIntoView (like Konqueror)
 950}
 951
 952/**
 953 * Returns true iff the current environment is the IDE.
 954 */
 955function is_IDE()
 956{
 957    return (typeof(SeleniumIDE) != 'undefined');
 958}
 959
 960/**
 961 * Logs a message if the Logger exists, and does nothing if it doesn't exist.
 962 *
 963 * @param level  the level to log at
 964 * @param msg    the message to log
 965 */
 966function safe_log(level, msg)
 967{
 968    try {
 969        LOG[level](msg);
 970    }
 971    catch (e) {
 972        // couldn't log!
 973    }
 974}
 975
 976/**
 977 * Displays a warning message to the user appropriate to the context under
 978 * which the issue is encountered. This is primarily used to avoid popping up
 979 * alert dialogs that might pause an automated test suite.
 980 *
 981 * @param msg  the warning message to display
 982 */
 983function safe_alert(msg)
 984{
 985    if (is_IDE()) {
 986        alert(msg);
 987    }
 988}
 989
 990//******************************************************************************
 991// Locator evaluation support
 992
 993/**
 994 * Parses a Selenium locator, returning its type and the unprefixed locator
 995 * string as an object.
 996 *
 997 * @param locator  the locator to parse
 998 */
 999function parse_locator(locator)
1000{
1001    var result = locator.match(/^([A-Za-z]+)=(.+)/);
1002    if (result) {
1003        return { type: result[1].toLowerCase(), string: result[2] };
1004    }
1005    return { type: 'implicit', string: locator };
1006}
1007
1008/**
1009 * Evaluates an xpath on a document, and returns a list containing nodes in the
1010 * resulting nodeset. The browserbot xpath methods are now backed by this
1011 * function. A context node may optionally be provided, and the xpath will be
1012 * evaluated from that context.
1013 *
1014 * @param xpath       the xpath to evaluate
1015 * @param inDocument  the document in which to evaluate the xpath.
1016 * @param opts        (optional) An object containing various flags that can
1017 *                    modify how the xpath is evaluated. Here's a listing of
1018 *                    the meaningful keys:
1019 *
1020 *                     contextNode: 
1021 *                       the context node from which to evaluate the xpath. If
1022 *                       unspecified, the context will be the root document
1023 *                       element.
1024 *
1025 *                     namespaceResolver:
1026 *                       the namespace resolver function. Defaults to null.
1027 *
1028 *                     xpathLibrary:
1029 *                       the javascript library to use for XPath. "ajaxslt" is
1030 *                       the default. "javascript-xpath" is newer and faster,
1031 *                       but needs more testing.
1032 *
1033 *                     allowNativeXpath:
1034 *                       whether to allow native evaluate(). Defaults to true.
1035 *
1036 *                     ignoreAttributesWithoutValue:
1037 *                       whether it's ok to ignore attributes without value
1038 *                       when evaluating the xpath. This can greatly improve
1039 *                       performance in IE; however, if your xpaths depend on
1040 *                       such attributes, you can't ignore them! Defaults to
1041 *                       true.
1042 *
1043 *                     returnOnFirstMatch:
1044 *                       whether to optimize the XPath evaluation to only
1045 *                       return the first match. The match, if any, will still
1046 *                       be returned in a list. Defaults to false.
1047 */
1048function eval_xpath(xpath, inDocument, opts)
1049{
1050    if (!opts) {
1051        var opts = {};
1052    }
1053    var contextNode = opts.contextNode
1054        ? opts.contextNode : inDocument;
1055    var namespaceResolver = opts.namespaceResolver
1056        ? opts.namespaceResolver : null;
1057    var xpathLibrary = opts.xpathLibrary
1058        ? opts.xpathLibrary : null;
1059    var allowNativeXpath = (opts.allowNativeXpath != undefined)
1060        ? opts.allowNativeXpath : true;
1061    var ignoreAttributesWithoutValue = (opts.ignoreAttributesWithoutValue != undefined)
1062        ? opts.ignoreAttributesWithoutValue : true;
1063    var returnOnFirstMatch = (opts.returnOnFirstMatch != undefined)
1064        ? opts.returnOnFirstMatch : false;
1065
1066    // Trim any trailing "/": not valid xpath, and remains from attribute
1067    // locator.
1068    if (xpath.charAt(xpath.length - 1) == '/') {
1069        xpath = xpath.slice(0, -1);
1070    }
1071    // HUGE hack - remove namespace from xpath for IE
1072    if (browserVersion && browserVersion.isIE) {
1073        xpath = xpath.replace(/x:/g, '')
1074    }
1075
1076    // When using the new and faster javascript-xpath library,
1077    // we'll use the TestRunner's document object, not the App-Under-Test's document.
1078    // The new library only modifies the TestRunner document with the new 
1079    // functionality.
1080    if (xpathLibrary == 'javascript-xpath') {
1081        documentForXpath = document;
1082    } else {
1083        documentForXpath = inDocument;
1084    }
1085    var results = [];
1086    
1087    // Use document.evaluate() if it's available
1088    if (allowNativeXpath && documentForXpath.evaluate) {
1089        try {
1090            // Regarding use of the second argument to document.evaluate():
1091            // http://groups.google.com/group/comp.lang.javascript/browse_thread/thread/a59ce20639c74ba1/a9d9f53e88e5ebb5
1092            var xpathResult = documentForXpath
1093                .evaluate((contextNode == inDocument ? xpath : '.' + xpath),
1094                    contextNode, namespaceResolver, 0, null);
1095        }
1096        catch (e) {
1097            throw new SeleniumError("Invalid xpath: " + extractExceptionMessage(e));
1098        }
1099        finally{
1100            if (xpathResult == null) {
1101                // If the result is null, we should still throw an Error.
1102                throw new SeleniumError("Invalid xpath: " + xpath); 
1103            }
1104        }
1105        var result = xpathResult.iterateNext();
1106        while (result) {
1107            results.push(result);
1108            result = xpathResult.iterateNext();
1109        }
1110        return results;
1111    }
1112
1113    // If not, fall back to slower JavaScript implementation
1114    // DGF set xpathdebug = true (using getEval, if you like) to turn on JS XPath debugging
1115    //xpathdebug = true;
1116    var context;
1117    if (contextNode == inDocument) {
1118        context = new ExprContext(inDocument);
1119    }
1120    else {
1121        // provide false values to get the default constructor values
1122        context = new ExprContext(contextNode, false, false,
1123            contextNode.parentNode);
1124    }
1125    context.setCaseInsensitive(true);
1126    context.setIgnoreAttributesWithoutValue(ignoreAttributesWithoutValue);
1127    context.setReturnOnFirstMatch(returnOnFirstMatch);
1128    var xpathObj;
1129    try {
1130        xpathObj = xpathParse(xpath);
1131    }
1132    catch (e) {
1133        throw new SeleniumError("Invalid xpath: " + extractExceptionMessage(e));
1134    }
1135    var xpathResult = xpathObj.evaluate(context);
1136    if (xpathResult && xpathResult.value) {
1137        for (var i = 0; i < xpathResult.value.length; ++i) {
1138            results.push(xpathResult.value[i]);
1139        }
1140    }
1141    return results;
1142}
1143
1144/**
1145 * Returns the full resultset of a CSS selector evaluation.
1146 */
1147function eval_css(locator, inDocument)
1148{
1149    return cssQuery(locator, inDocument);
1150}
1151
1152/**
1153 * This function duplicates part of BrowserBot.findElement() to open up locator
1154 * evaluation on arbitrary documents. It returns a plain old array of located
1155 * elements found by using a Selenium locator.
1156 * 
1157 * Multiple results may be generated for xpath and CSS locators. Even though a
1158 * list could potentially be generated for other locator types, such as link,
1159 * we don't try for them, because they aren't very expressive location
1160 * strategies; if you want a list, use xpath or CSS. Furthermore, strategies
1161 * for these locators have been optimized to only return the first result. For
1162 * these types of locators, performance is more important than ideal behavior.
1163 * 
1164 * @param locator          a locator string
1165 * @param inDocument       the document in which to apply the locator
1166 * @param opt_contextNode  the context within which to evaluate the locator
1167 *
1168 * @return  a list of result elements
1169 */
1170function eval_locator(locator, inDocument, opt_contextNode)
1171{
1172    locator = parse_locator(locator);
1173    
1174    var pageBot;
1175    if (typeof(selenium) != 'undefined' && selenium != undefined) {
1176        if (typeof(editor) == 'undefined' || editor.state == 'playing') {
1177            safe_log('info', 'Trying [' + locator.type + ']: '
1178                + locator.string);
1179        }
1180        pageBot = selenium.browserbot;
1181    }
1182    else {
1183        if (!UI_GLOBAL.mozillaBrowserBot) {
1184            // create a browser bot to evaluate the locator. Hand it the IDE
1185            // window as a dummy window, and cache it for future use.
1186            UI_GLOBAL.mozillaBrowserBot = new MozillaBrowserBot(window)
1187        }
1188        pageBot = UI_GLOBAL.mozillaBrowserBot;
1189    }
1190    
1191    var results = [];
1192    
1193    if (locator.type == 'xpath' || (locator.string.charAt(0) == '/' &&
1194        locator.type == 'implicit')) {
1195        results = eval_xpath(locator.string, inDocument,
1196            { contextNode: opt_contextNode });
1197    }
1198    else if (locator.type == 'css') {
1199        results = eval_css(locator.string, inDocument);
1200    }
1201    else {
1202        var element = pageBot
1203            .findElementBy(locator.type, locator.string, inDocument);
1204        if (element != null) {
1205            results.push(element);
1206        }
1207    }
1208    
1209    return results;
1210}
1211
1212//******************************************************************************
1213// UI-Element
1214
1215/**
1216 * Escapes the special regular expression characters in a string intended to be
1217 * used as a regular expression.
1218 *
1219 * Based on: http://simonwillison.net/2006/Jan/20/escape/
1220 */
1221RegExp.escape = (function() {
1222    var specials = [
1223        '/', '.', '*', '+', '?', '|', '^', '$',
1224        '(', ')', '[', ']', '{', '}', '\\'
1225    ];
1226    
1227    var sRE = new RegExp(
1228        '(\\' + specials.join('|\\') + ')', 'g'
1229    );
1230  
1231    return function(text) {
1232        return text.replace(sRE, '\\$1');
1233    }
1234})();
1235
1236/**
1237 * Returns true if two arrays are identical, and false otherwise.
1238 *
1239 * @param a1  the first array, may only contain simple values (strings or
1240 *            numbers)
1241 * @param a2  the second array, same restricts on data as for a1
1242 * @return    true if the arrays are equivalent, false otherwise.
1243 */
1244function are_equal(a1, a2)
1245{
1246    if (typeof(a1) != typeof(a2))
1247        return false;
1248    
1249    switch(typeof(a1)) {
1250        case 'object':
1251            // arrays
1252            if (a1.length) {
1253                if (a1.length != a2.length)
1254                    return false;
1255                for (var i = 0; i < a1.length; ++i) {
1256                    if (!are_equal(a1[i], a2[i]))
1257                        return false
1258                }
1259            }
1260            // associative arrays
1261            else {
1262                var keys = {};
1263                for (var key in a1) {
1264                    keys[key] = true;
1265                }
1266                for (var key in a2) {
1267                    keys[key] = true;
1268                }
1269                for (var key in keys) {
1270                    if (!are_equal(a1[key], a2[key]))
1271                        return false;
1272                }
1273            }
1274            return true;
1275            
1276        default:
1277            return a1 == a2;
1278    }
1279}
1280
1281
1282/**
1283 * Create a clone of an object and return it. This is a deep copy of everything
1284 * but functions, whose references are copied. You shouldn't expect a deep copy
1285 * of functions anyway.
1286 *
1287 * @param orig  the original object to copy
1288 * @return      a deep copy of the original object. Any functions attached,
1289 *              however, will have their references copied only.
1290 */
1291function clone(orig) {
1292    var copy;
1293    switch(typeof(orig)) {
1294        case 'object':
1295            copy = (orig.length) ? [] : {};
1296            for (var attr in orig) {
1297                copy[attr] = clone(orig[attr]);
1298            }
1299            break;
1300        default:
1301            copy = orig;
1302            break;
1303    }
1304    return copy;
1305}
1306
1307/**
1308 * Emulates php's print_r() functionality. Returns a nicely formatted string
1309 * representation of an object. Very useful for debugging.
1310 *
1311 * @param object    the object to dump
1312 * @param maxDepth  the maximum depth to recurse into the object. Ellipses will
1313 *                  be shown for objects whose depth exceeds the maximum.
1314 * @param indent    the string to use for indenting progressively deeper levels
1315 *                  of the dump.
1316 * @return          a string representing a dump of the object
1317 */
1318function print_r(object, maxDepth, indent)
1319{
1320    var parentIndent, attr, str = "";
1321    if (arguments.length == 1) {
1322        var maxDepth = Number.MAX_VALUE;
1323    } else {
1324        maxDepth--;
1325    }
1326    if (arguments.length < 3) {
1327        parentIndent = ''
1328        var indent = '    ';
1329    } else {
1330        parentIndent = indent;
1331        indent += '    ';
1332    }
1333
1334    switch(typeof(object)) {
1335    case 'object':
1336        if (object.length != undefined) {
1337            if (object.length == 0) {
1338                str += "Array ()\r\n";
1339            }
1340            else {
1341                str += "Array (\r\n";
1342                for (var i = 0; i < object.length; ++i) {
1343                    str += indent + '[' + i + '] => ';
1344                    if (maxDepth == 0)
1345                        str += "...\r\n";
1346                    else
1347                        str += print_r(object[i], maxDepth, indent);
1348                }
1349                str += parentIndent + ")\r\n";
1350            }
1351        }
1352        else {
1353            str += "Object (\r\n";
1354            for (attr in object) {
1355                str += indent + "[" + attr + "] => ";
1356                if (maxDepth == 0)
1357                    str += "...\r\n";
1358                else
1359                    str += print_r(object[attr], maxDepth, indent);
1360            }
1361            str += parentIndent + ")\r\n";
1362        }
1363        break;
1364    case 'boolean':
1365        str += (object ? 'true' : 'false') + "\r\n";
1366        break;
1367    case 'function':
1368        str += "Function\r\n";
1369        break;
1370    default:
1371        str += object + "\r\n";
1372        break;
1373
1374    }
1375    return str;
1376}
1377
1378/**
1379 * Return an array containing all properties of an object. Perl-style.
1380 *
1381 * @param object  the object whose keys to return
1382 * @return        array of object keys, as strings
1383 */
1384function keys(object)
1385{
1386    var keys = [];
1387    for (var k in object) {
1388        keys.push(k);
1389    }
1390    return keys;
1391}
1392
1393/**
1394 * Emulates python's range() built-in. Returns an array of integers, counting
1395 * up (or down) from start to end. Note that the range returned is up to, but
1396 * NOT INCLUDING, end.
1397 *.
1398 * @param start  integer from which to start counting. If the end parameter is
1399 *               not provided, this value is considered the end and start will
1400 *               be zero.
1401 * @param end    integer to which to count. If omitted, the function will count
1402 *               up from zero to the value of the start parameter. Note that
1403 *               the array returned will count up to but will not include this
1404 *               value.
1405 * @return       an array of consecutive integers. 
1406 */
1407function range(start, end)
1408{
1409    if (arguments.length == 1) {
1410        var end = start;
1411        start = 0;
1412    }
1413    
1414    var r = [];
1415    if (start < end) {
1416        while (start != end)
1417            r.push(start++);
1418    }
1419    else {
1420        while (start != end)
1421            r.push(start--);
1422    }
1423    return r;
1424}
1425
1426/**
1427 * Parses a python-style keyword arguments string and returns the pairs in a
1428 * new object.
1429 *
1430 * @param  kwargs  a string representing a set of keyword arguments. It should
1431 *                 look like <tt>keyword1=value1, keyword2=value2, ...</tt>
1432 * @return         an object mapping strings to strings
1433 */
1434function parse_kwargs(kwargs)
1435{
1436    var args = new Object();
1437    var pairs = kwargs.split(/,/);
1438    for (var i = 0; i < pairs.length;) {
1439        if (i > 0 && pairs[i].indexOf('=') == -1) {
1440            // the value string contained a comma. Glue the parts back together.
1441            pairs[i-1] += ',' + pairs.splice(i, 1)[0];
1442        }
1443        else {
1444            ++i;
1445        }
1446    }
1447    for (var i = 0; i < pairs.length; ++i) {
1448        var splits = pairs[i].split(/=/);
1449        if (splits.length == 1) {
1450            continue;
1451        }
1452        var key = splits.shift();
1453        var value = splits.join('=');
1454        args[key.trim()] = value.trim();
1455    }
1456    return args;
1457}
1458
1459/**
1460 * Creates a python-style keyword arguments string from an object.
1461 *
1462 * @param args        an associative array mapping strings to strings
1463 * @param sortedKeys  (optional) a list of keys of the args parameter that
1464 *                    specifies the order in which the arguments will appear in
1465 *                    the returned kwargs string
1466 *
1467 * @return            a kwarg string representation of args
1468 */
1469function to_kwargs(args, sortedKeys)
1470{
1471    var s = '';
1472    if (!sortedKeys) {
1473        var sortedKeys = keys(args).sort();
1474    }
1475    for (var i = 0; i < sortedKeys.length; ++i) {
1476        var k = sortedKeys[i];
1477        if (args[k] != undefined) {
1478            if (s) {
1479                s += ', ';
1480            }
1481            s += k + '=' + args[k];
1482        }
1483    }
1484    return s;
1485}
1486
1487/**
1488 * Returns true if a node is an ancestor node of a target node, and false
1489 * otherwise.
1490 *
1491 * @param node    the node being compared to the target node
1492 * @param target  the target node
1493 * @return        true if node is an ancestor node of target, false otherwise.
1494 */
1495function is_ancestor(node, target)
1496{
1497    while (target.parentNode) {
1498        target = target.parentNode;
1499        if (node == target)
1500            return true;
1501    }
1502    return false;
1503}
1504
1505//******************************************************************************
1506// parseUri 1.2.1
1507// MIT License
1508
1509/*
1510Copyright (c) 2007 Steven Levithan <stevenlevithan.com>
1511
1512Permission is hereby granted, free of charge, to any person obtaining a copy
1513of this software and associated documentation files (the "Software"), to deal
1514in the Software without restriction, including without limitation the rights
1515to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
1516copies of the Software, and to permit persons to whom the Software is
1517furnished to do so, subject to the following conditions:
1518
1519The above copyright notice and this permission notice shall be included in
1520all copies or substantial portions of the Software.
1521*/
1522
1523function parseUri (str) {
1524    var o   = parseUri.options,
1525        m   = o.parser[o.strictMode ? "strict" : "loose"].exec(str),
1526        uri = {},
1527        i   = 14;
1528
1529    while (i--) uri[o.key[i]] = m[i] || "";
1530
1531    uri[o.q.name] = {};
1532    uri[o.key[12]].replace(o.q.parser, function ($0, $1, $2) {
1533        if ($1) uri[o.q.name][$1] = $2;
1534    });
1535
1536    return uri;
1537};
1538
1539parseUri.options = {
1540    strictMode: false,
1541    key: ["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"],
1542    q:   {
1543        name:   "queryKey",
1544        parser: /(?:^|&)([^&=]*)=?([^&]*)/g
1545    },
1546    parser: {
1547        strict: /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*):?([^:@]*))?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/,
1548        loose:  /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*):?([^:@]*))?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/
1549    }
1550};
1551