// xpath.js - version 0.7 - Spry Pre-Release 1.6.1
//
// Code from xmltoken.js.
//
// Copyright 2006 Google Inc.
// All Rights Reserved
//
// Defines regular expression patterns to extract XML tokens from string.
// See <http://www.w3.org/TR/REC-xml/#sec-common-syn>,
// <http://www.w3.org/TR/xml11/#sec-common-syn> and
// <http://www.w3.org/TR/REC-xml-names/#NT-NCName> for the specifications.
//
// Author: Junji Takagi <jtakagi@google.com>

// Detect whether RegExp supports Unicode characters or not.

var REGEXP_UNICODE = function() {
  var tests = [' ', '\u0120', -1,  // Konquerer 3.4.0 fails here.
               '!', '\u0120', -1,
               '\u0120', '\u0120', 0,
               '\u0121', '\u0120', -1,
               '\u0121', '\u0120|\u0121', 0,
               '\u0122', '\u0120|\u0121', -1,
               '\u0120', '[\u0120]', 0,  // Safari 2.0.3 fails here.
               '\u0121', '[\u0120]', -1,
               '\u0121', '[\u0120\u0121]', 0,  // Safari 2.0.3 fails here.
               '\u0122', '[\u0120\u0121]', -1,
               '\u0121', '[\u0120-\u0121]', 0,  // Safari 2.0.3 fails here.
               '\u0122', '[\u0120-\u0121]', -1];
  for (var i = 0; i < tests.length; i += 3) {
    if (tests[i].search(new RegExp(tests[i + 1])) != tests[i + 2]) {
      return false;
    }
  }
  return true;
}();

// Common tokens in XML 1.0 and XML 1.1.

var XML_S = '[ \t\r\n]+';
var XML_EQ = '(' + XML_S + ')?=(' + XML_S + ')?';
var XML_CHAR_REF = '&#[0-9]+;|&#x[0-9a-fA-F]+;';

// XML 1.0 tokens.

var XML10_VERSION_INFO = XML_S + 'version' + XML_EQ + '("1\\.0"|' + "'1\\.0')";
var XML10_BASE_CHAR = (REGEXP_UNICODE) ?
  '\u0041-\u005a\u0061-\u007a\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u00ff' +
  '\u0100-\u0131\u0134-\u013e\u0141-\u0148\u014a-\u017e\u0180-\u01c3' +
  '\u01cd-\u01f0\u01f4-\u01f5\u01fa-\u0217\u0250-\u02a8\u02bb-\u02c1\u0386' +
  '\u0388-\u038a\u038c\u038e-\u03a1\u03a3-\u03ce\u03d0-\u03d6\u03da\u03dc' +
  '\u03de\u03e0\u03e2-\u03f3\u0401-\u040c\u040e-\u044f\u0451-\u045c' +
  '\u045e-\u0481\u0490-\u04c4\u04c7-\u04c8\u04cb-\u04cc\u04d0-\u04eb' +
  '\u04ee-\u04f5\u04f8-\u04f9\u0531-\u0556\u0559\u0561-\u0586\u05d0-\u05ea' +
  '\u05f0-\u05f2\u0621-\u063a\u0641-\u064a\u0671-\u06b7\u06ba-\u06be' +
  '\u06c0-\u06ce\u06d0-\u06d3\u06d5\u06e5-\u06e6\u0905-\u0939\u093d' +
  '\u0958-\u0961\u0985-\u098c\u098f-\u0990\u0993-\u09a8\u09aa-\u09b0\u09b2' +
  '\u09b6-\u09b9\u09dc-\u09dd\u09df-\u09e1\u09f0-\u09f1\u0a05-\u0a0a' +
  '\u0a0f-\u0a10\u0a13-\u0a28\u0a2a-\u0a30\u0a32-\u0a33\u0a35-\u0a36' +
  '\u0a38-\u0a39\u0a59-\u0a5c\u0a5e\u0a72-\u0a74\u0a85-\u0a8b\u0a8d' +
  '\u0a8f-\u0a91\u0a93-\u0aa8\u0aaa-\u0ab0\u0ab2-\u0ab3\u0ab5-\u0ab9' +
  '\u0abd\u0ae0\u0b05-\u0b0c\u0b0f-\u0b10\u0b13-\u0b28\u0b2a-\u0b30' +
  '\u0b32-\u0b33\u0b36-\u0b39\u0b3d\u0b5c-\u0b5d\u0b5f-\u0b61\u0b85-\u0b8a' +
  '\u0b8e-\u0b90\u0b92-\u0b95\u0b99-\u0b9a\u0b9c\u0b9e-\u0b9f\u0ba3-\u0ba4' +
  '\u0ba8-\u0baa\u0bae-\u0bb5\u0bb7-\u0bb9\u0c05-\u0c0c\u0c0e-\u0c10' +
  '\u0c12-\u0c28\u0c2a-\u0c33\u0c35-\u0c39\u0c60-\u0c61\u0c85-\u0c8c' +
  '\u0c8e-\u0c90\u0c92-\u0ca8\u0caa-\u0cb3\u0cb5-\u0cb9\u0cde\u0ce0-\u0ce1' +
  '\u0d05-\u0d0c\u0d0e-\u0d10\u0d12-\u0d28\u0d2a-\u0d39\u0d60-\u0d61' +
  '\u0e01-\u0e2e\u0e30\u0e32-\u0e33\u0e40-\u0e45\u0e81-\u0e82\u0e84' +
  '\u0e87-\u0e88\u0e8a\u0e8d\u0e94-\u0e97\u0e99-\u0e9f\u0ea1-\u0ea3\u0ea5' +
  '\u0ea7\u0eaa-\u0eab\u0ead-\u0eae\u0eb0\u0eb2-\u0eb3\u0ebd\u0ec0-\u0ec4' +
  '\u0f40-\u0f47\u0f49-\u0f69\u10a0-\u10c5\u10d0-\u10f6\u1100\u1102-\u1103' +
  '\u1105-\u1107\u1109\u110b-\u110c\u110e-\u1112\u113c\u113e\u1140\u114c' +
  '\u114e\u1150\u1154-\u1155\u1159\u115f-\u1161\u1163\u1165\u1167\u1169' +
  '\u116d-\u116e\u1172-\u1173\u1175\u119e\u11a8\u11ab\u11ae-\u11af' +
  '\u11b7-\u11b8\u11ba\u11bc-\u11c2\u11eb\u11f0\u11f9\u1e00-\u1e9b' +
  '\u1ea0-\u1ef9\u1f00-\u1f15\u1f18-\u1f1d\u1f20-\u1f45\u1f48-\u1f4d' +
  '\u1f50-\u1f57\u1f59\u1f5b\u1f5d\u1f5f-\u1f7d\u1f80-\u1fb4\u1fb6-\u1fbc' +
  '\u1fbe\u1fc2-\u1fc4\u1fc6-\u1fcc\u1fd0-\u1fd3\u1fd6-\u1fdb\u1fe0-\u1fec' +
  '\u1ff2-\u1ff4\u1ff6-\u1ffc\u2126\u212a-\u212b\u212e\u2180-\u2182' +
  '\u3041-\u3094\u30a1-\u30fa\u3105-\u312c\uac00-\ud7a3' :
  'A-Za-z';
var XML10_IDEOGRAPHIC = (REGEXP_UNICODE) ?
  '\u4e00-\u9fa5\u3007\u3021-\u3029' :
  '';
var XML10_COMBINING_CHAR = (REGEXP_UNICODE) ?
  '\u0300-\u0345\u0360-\u0361\u0483-\u0486\u0591-\u05a1\u05a3-\u05b9' +
  '\u05bb-\u05bd\u05bf\u05c1-\u05c2\u05c4\u064b-\u0652\u0670\u06d6-\u06dc' +
  '\u06dd-\u06df\u06e0-\u06e4\u06e7-\u06e8\u06ea-\u06ed\u0901-\u0903\u093c' +
  '\u093e-\u094c\u094d\u0951-\u0954\u0962-\u0963\u0981-\u0983\u09bc\u09be' +
  '\u09bf\u09c0-\u09c4\u09c7-\u09c8\u09cb-\u09cd\u09d7\u09e2-\u09e3\u0a02' +
  '\u0a3c\u0a3e\u0a3f\u0a40-\u0a42\u0a47-\u0a48\u0a4b-\u0a4d\u0a70-\u0a71' +
  '\u0a81-\u0a83\u0abc\u0abe-\u0ac5\u0ac7-\u0ac9\u0acb-\u0acd\u0b01-\u0b03' +
  '\u0b3c\u0b3e-\u0b43\u0b47-\u0b48\u0b4b-\u0b4d\u0b56-\u0b57\u0b82-\u0b83' +
  '\u0bbe-\u0bc2\u0bc6-\u0bc8\u0bca-\u0bcd\u0bd7\u0c01-\u0c03\u0c3e-\u0c44' +
  '\u0c46-\u0c48\u0c4a-\u0c4d\u0c55-\u0c56\u0c82-\u0c83\u0cbe-\u0cc4' +
  '\u0cc6-\u0cc8\u0cca-\u0ccd\u0cd5-\u0cd6\u0d02-\u0d03\u0d3e-\u0d43' +
  '\u0d46-\u0d48\u0d4a-\u0d4d\u0d57\u0e31\u0e34-\u0e3a\u0e47-\u0e4e\u0eb1' +
  '\u0eb4-\u0eb9\u0ebb-\u0ebc\u0ec8-\u0ecd\u0f18-\u0f19\u0f35\u0f37\u0f39' +
  '\u0f3e\u0f3f\u0f71-\u0f84\u0f86-\u0f8b\u0f90-\u0f95\u0f97\u0f99-\u0fad' +
  '\u0fb1-\u0fb7\u0fb9\u20d0-\u20dc\u20e1\u302a-\u302f\u3099\u309a' :
  '';
var XML10_DIGIT = (REGEXP_UNICODE) ?
  '\u0030-\u0039\u0660-\u0669\u06f0-\u06f9\u0966-\u096f\u09e6-\u09ef' +
  '\u0a66-\u0a6f\u0ae6-\u0aef\u0b66-\u0b6f\u0be7-\u0bef\u0c66-\u0c6f' +
  '\u0ce6-\u0cef\u0d66-\u0d6f\u0e50-\u0e59\u0ed0-\u0ed9\u0f20-\u0f29' :
  '0-9';
var XML10_EXTENDER = (REGEXP_UNICODE) ?
  '\u00b7\u02d0\u02d1\u0387\u0640\u0e46\u0ec6\u3005\u3031-\u3035' +
  '\u309d-\u309e\u30fc-\u30fe' :
  '';
var XML10_LETTER = XML10_BASE_CHAR + XML10_IDEOGRAPHIC;
var XML10_NAME_CHAR = XML10_LETTER + XML10_DIGIT + '\\._:' +
                      XML10_COMBINING_CHAR + XML10_EXTENDER + '-';
var XML10_NAME = '[' + XML10_LETTER + '_:][' + XML10_NAME_CHAR + ']*';

var XML10_ENTITY_REF = '&' + XML10_NAME + ';';
var XML10_REFERENCE = XML10_ENTITY_REF + '|' + XML_CHAR_REF;
var XML10_ATT_VALUE = '"(([^<&"]|' + XML10_REFERENCE + ')*)"|' +
                      "'(([^<&']|" + XML10_REFERENCE + ")*)'";
var XML10_ATTRIBUTE =
  '(' + XML10_NAME + ')' + XML_EQ + '(' + XML10_ATT_VALUE + ')';

// XML 1.1 tokens.
// TODO(jtakagi): NameStartChar also includes \u10000-\ueffff.
// ECMAScript Language Specifiction defines UnicodeEscapeSequence as
// "\u HexDigit HexDigit HexDigit HexDigit" and we may need to use
// surrogate pairs, but any browser doesn't support surrogate paris in
// character classes of regular expression, so avoid including them for now.

var XML11_VERSION_INFO = XML_S + 'version' + XML_EQ + '("1\\.1"|' + "'1\\.1')";
var XML11_NAME_START_CHAR = (REGEXP_UNICODE) ?
  ':A-Z_a-z\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u02ff\u0370-\u037d' +
  '\u037f-\u1fff\u200c-\u200d\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff' +
  '\uf900-\ufdcf\ufdf0-\ufffd' :
  ':A-Z_a-z';
var XML11_NAME_CHAR = XML11_NAME_START_CHAR +
  ((REGEXP_UNICODE) ? '\\.0-9\u00b7\u0300-\u036f\u203f-\u2040-' : '\\.0-9-');
var XML11_NAME = '[' + XML11_NAME_START_CHAR + '][' + XML11_NAME_CHAR + ']*';

var XML11_ENTITY_REF = '&' + XML11_NAME + ';';
var XML11_REFERENCE = XML11_ENTITY_REF + '|' + XML_CHAR_REF;
var XML11_ATT_VALUE = '"(([^<&"]|' + XML11_REFERENCE + ')*)"|' +
                      "'(([^<&']|" + XML11_REFERENCE + ")*)'";
var XML11_ATTRIBUTE =
  '(' + XML11_NAME + ')' + XML_EQ + '(' + XML11_ATT_VALUE + ')';

// XML Namespace tokens.
// Used in XML parser and XPath parser.

var XML_NC_NAME_CHAR = XML10_LETTER + XML10_DIGIT + '\\._' +
                       XML10_COMBINING_CHAR + XML10_EXTENDER + '-';
var XML_NC_NAME = '[' + XML10_LETTER + '_][' + XML_NC_NAME_CHAR + ']*';


// Code from dom.js.
//
// Based on <http://www.w3.org/TR/2000/REC-DOM-Level-2-Core-20001113/
// core.html#ID-1950641247>
var DOM_ELEMENT_NODE = 1;
var DOM_ATTRIBUTE_NODE = 2;
var DOM_TEXT_NODE = 3;
var DOM_CDATA_SECTION_NODE = 4;
var DOM_ENTITY_REFERENCE_NODE = 5;
var DOM_ENTITY_NODE = 6;
var DOM_PROCESSING_INSTRUCTION_NODE = 7;
var DOM_COMMENT_NODE = 8;
var DOM_DOCUMENT_NODE = 9;
var DOM_DOCUMENT_TYPE_NODE = 10;
var DOM_DOCUMENT_FRAGMENT_NODE = 11;
var DOM_NOTATION_NODE = 12;

// Code from util.js.
//
// Copyright 2005 Google
//
// Author: Steffen Meschkat <mesch@google.com>
//
// Miscellaneous utility and placeholder functions.

// Dummy implmentation for the logging functions. Replace by something
// useful when you want to debug.
function xpathLog(msg) {};
function xsltLog(msg) {};
function xsltLogXml(msg) {};

// Throws an exception if false.
function assert(b) {
  if (!b) {
    throw "Assertion failed";
  }
}

// Splits a string s at all occurrences of character c. This is like
// the split() method of the string object, but IE omits empty
// strings, which violates the invariant (s.split(x).join(x) == s).
function stringSplit(s, c) {
  var a = s.indexOf(c);
  if (a == -1) {
    return [ s ];
  }
  var parts = [];
  parts.push(s.substr(0,a));
  while (a != -1) {
    var a1 = s.indexOf(c, a + 1);
    if (a1 != -1) {
      parts.push(s.substr(a + 1, a1 - a - 1));
    } else {
      parts.push(s.substr(a + 1));
    }
    a = a1;
  }
  return parts;
}

// Applies the given function to each element of the array, preserving
// this, and passing the index.
function mapExec(array, func) {
  for (var i = 0; i < array.length; ++i) {
    func.call(this, array[i], i);
  }
}

// Returns an array that contains the return value of the given
// function applied to every element of the input array.
function mapExpr(array, func) {
  var ret = [];
  for (var i = 0; i < array.length; ++i) {
    ret.push(func(array[i]));
  }
  return ret;
};

// Reverses the given array in place.
function reverseInplace(array) {
  for (var i = 0; i < array.length / 2; ++i) {
    var h = array[i];
    var ii = array.length - i - 1;
    array[i] = array[ii];
    array[ii] = h;
  }
}

// Removes value from array. Returns the number of instances of value
// that were removed from array.
function removeFromArray(array, value, opt_notype) {
  var shift = 0;
  for (var i = 0; i < array.length; ++i) {
    if (array[i] === value || (opt_notype && array[i] == value)) {
      array.splice(i--, 1);
      shift++;
    }
  }
  return shift;
}

// Shallow-copies an array.
function copyArray(dst, src) {
  for (var i = 0; i < src.length; ++i) {
    dst.push(src[i]);
  }
}

// Returns the text value of a node; for nodes without children this
// is the nodeValue, for nodes with children this is the concatenation
// of the value of all children.
function xmlValue(node) {
  if (!node) {
    return '';
  }

  var ret = '';
  if (node.nodeType == DOM_TEXT_NODE ||
      node.nodeType == DOM_CDATA_SECTION_NODE ||
      node.nodeType == DOM_ATTRIBUTE_NODE) {
    ret += node.nodeValue;

  } else if (node.nodeType == DOM_ELEMENT_NODE ||
             node.nodeType == DOM_DOCUMENT_NODE ||
             node.nodeType == DOM_DOCUMENT_FRAGMENT_NODE) {
    for (var i = 0; i < node.childNodes.length; ++i) {
      ret += arguments.callee(node.childNodes[i]);
    }
  }
  return ret;
}

// Code from xpath.js.
//
// Copyright 2005 Google Inc.
// All Rights Reserved
//
// An XPath parser and evaluator written in JavaScript. The
// implementation is complete except for functions handling
// namespaces.
//
// Reference: [XPATH] XPath Specification
// <http://www.w3.org/TR/1999/REC-xpath-19991116>.
//
//
// The API of the parser has several parts:
//
// 1. The parser function xpathParse() that takes a string and returns
// an expession object.
//
// 2. The expression object that has an evaluate() method to evaluate the
// XPath expression it represents. (It is actually a hierarchy of
// objects that resembles the parse tree, but an application will call
// evaluate() only on the top node of this hierarchy.)
//
// 3. The context object that is passed as an argument to the evaluate()
// method, which represents the DOM context in which the expression is
// evaluated.
//
// 4. The value object that is returned from evaluate() and represents
// values of the different types that are defined by XPath (number,
// string, boolean, and node-set), and allows to convert between them.
//
// These parts are near the top of the file, the functions and data
// that are used internally follow after them.
//
//
// Author: Steffen Meschkat <mesch@google.com>


// The entry point for the parser.
//
// @param expr a string that contains an XPath expression.
// @return an expression object that can be evaluated with an
// expression context.

function xpathParse(expr) {
  xpathLog('parse ' + expr);
  xpathParseInit();

  var cached = xpathCacheLookup(expr);
  if (cached) {
    xpathLog(' ... cached');
    return cached;
  }

  // Optimize for a few common cases: simple attribute node tests
  // (@id), simple element node tests (page), variable references
  // ($address), numbers (4), multi-step path expressions where each
  // step is a plain element node test
  // (page/overlay/locations/location).

  if (expr.match(/^(\$|@)?\w+$/i)) {
    var ret = makeSimpleExpr(expr);
    xpathParseCache[expr] = ret;
    xpathLog(' ... simple');
    return ret;
  }

  if (expr.match(/^\w+(\/\w+)*$/i)) {
    var ret = makeSimpleExpr2(expr);
    xpathParseCache[expr] = ret;
    xpathLog(' ... simple 2');
    return ret;
  }

  var cachekey = expr; // expr is modified during parse

  var stack = [];
  var ahead = null;
  var previous = null;
  var done = false;

  var parse_count = 0;
  var lexer_count = 0;
  var reduce_count = 0;

  while (!done) {
    parse_count++;
    expr = expr.replace(/^\s*/, '');
    previous = ahead;
    ahead = null;

    var rule = null;
    var match = '';
    for (var i = 0; i < xpathTokenRules.length; ++i) {
      var result = xpathTokenRules[i].re.exec(expr);
      lexer_count++;
      if (result && result.length > 0 && result[0].length > match.length) {
        rule = xpathTokenRules[i];
        match = result[0];
        break;
      }
    }

    // Special case: allow operator keywords to be element and
    // variable names.

    // NOTE(mesch): The parser resolves conflicts by looking ahead,
    // and this is the only case where we look back to
    // disambiguate. So this is indeed something different, and
    // looking back is usually done in the lexer (via states in the
    // general case, called "start conditions" in flex(1)). Also,the
    // conflict resolution in the parser is not as robust as it could
    // be, so I'd like to keep as much off the parser as possible (all
    // these precedence values should be computed from the grammar
    // rules and possibly associativity declarations, as in bison(1),
    // and not explicitly set.

    if (rule &&
        (rule == TOK_DIV ||
         rule == TOK_MOD ||
         rule == TOK_AND ||
         rule == TOK_OR) &&
        (!previous ||
         previous.tag == TOK_AT ||
         previous.tag == TOK_DSLASH ||
         previous.tag == TOK_SLASH ||
         previous.tag == TOK_AXIS ||
         previous.tag == TOK_DOLLAR)) {
      rule = TOK_QNAME;
    }

    if (rule) {
      expr = expr.substr(match.length);
      xpathLog('token: ' + match + ' -- ' + rule.label);
      ahead = {
        tag: rule,
        match: match,
        prec: rule.prec ?  rule.prec : 0, // || 0 is removed by the compiler
        expr: makeTokenExpr(match)
      };

    } else {
      xpathLog('DONE');
      done = true;
    }

    while (xpathReduce(stack, ahead)) {
      reduce_count++;
      xpathLog('stack: ' + stackToString(stack));
    }
  }

  xpathLog('stack: ' + stackToString(stack));

  if (stack.length != 1) {
    throw 'XPath parse error ' + cachekey + ':\n' + stackToString(stack);
  }

  var result = stack[0].expr;
  xpathParseCache[cachekey] = result;

  xpathLog('XPath parse: ' + parse_count + ' / ' +
           lexer_count + ' / ' + reduce_count);

  return result;
}

var xpathParseCache = {};

function xpathCacheLookup(expr) {
  return xpathParseCache[expr];
}

function xpathReduce(stack, ahead) {
  var cand = null;

  if (stack.length > 0) {
    var top = stack[stack.length-1];
    var ruleset = xpathRules[top.tag.key];

    if (ruleset) {
      for (var i = 0; i < ruleset.length; ++i) {
        var rule = ruleset[i];
        var match = xpathMatchStack(stack, rule[1]);
        if (match.length) {
          cand = {
            tag: rule[0],
            rule: rule,
            match: match
          };
          cand.prec = xpathGrammarPrecedence(cand);
          break;
        }
      }
    }
  }

  var ret;
  if (cand && (!ahead || cand.prec > ahead.prec ||
               (ahead.tag.left && cand.prec >= ahead.prec))) {
    for (var i = 0; i < cand.match.matchlength; ++i) {
      stack.pop();
    }

    xpathLog('reduce ' + cand.tag.label + ' ' + cand.prec +
             ' ahead ' + (ahead ? ahead.tag.label + ' ' + ahead.prec +
                          (ahead.tag.left ? ' left' : '')
                          : ' none '));

    var matchexpr = mapExpr(cand.match, function(m) { return m.expr; });
    cand.expr = cand.rule[3].apply(null, matchexpr);

    stack.push(cand);
    ret = true;

  } else {
    if (ahead) {
      xpathLog('shift ' + ahead.tag.label + ' ' + ahead.prec +
               (ahead.tag.left ? ' left' : '') +
               ' over ' + (cand ? cand.tag.label + ' ' +
                           cand.prec : ' none'));
      stack.push(ahead);
    }
    ret = false;
  }
  return ret;
}

function xpathMatchStack(stack, pattern) {

  // NOTE(mesch): The stack matches for variable cardinality are
  // greedy but don't do backtracking. This would be an issue only
  // with rules of the form A* A, i.e. with an element with variable
  // cardinality followed by the same element. Since that doesn't
  // occur in the grammar at hand, all matches on the stack are
  // unambiguous.

  var S = stack.length;
  var P = pattern.length;
  var p, s;
  var match = [];
  match.matchlength = 0;
  var ds = 0;
  for (p = P - 1, s = S - 1; p >= 0 && s >= 0; --p, s -= ds) {
    ds = 0;
    var qmatch = [];
    if (pattern[p] == Q_MM) {
      p -= 1;
      match.push(qmatch);
      while (s - ds >= 0 && stack[s - ds].tag == pattern[p]) {
        qmatch.push(stack[s - ds]);
        ds += 1;
        match.matchlength += 1;
      }

    } else if (pattern[p] == Q_01) {
      p -= 1;
      match.push(qmatch);
      while (s - ds >= 0 && ds < 2 && stack[s - ds].tag == pattern[p]) {
        qmatch.push(stack[s - ds]);
        ds += 1;
        match.matchlength += 1;
      }

    } else if (pattern[p] == Q_1M) {
      p -= 1;
      match.push(qmatch);
      if (stack[s].tag == pattern[p]) {
        while (s - ds >= 0 && stack[s - ds].tag == pattern[p]) {
          qmatch.push(stack[s - ds]);
          ds += 1;
          match.matchlength += 1;
        }
      } else {
        return [];
      }

    } else if (stack[s].tag == pattern[p]) {
      match.push(stack[s]);
      ds += 1;
      match.matchlength += 1;

    } else {
      return [];
    }

    reverseInplace(qmatch);
    qmatch.expr = mapExpr(qmatch, function(m) { return m.expr; });
  }

  reverseInplace(match);

  if (p == -1) {
    return match;

  } else {
    return [];
  }
}

function xpathTokenPrecedence(tag) {
  return tag.prec || 2;
}

function xpathGrammarPrecedence(frame) {
  var ret = 0;

  if (frame.rule) { /* normal reduce */
    if (frame.rule.length >= 3 && frame.rule[2] >= 0) {
      ret = frame.rule[2];

    } else {
      for (var i = 0; i < frame.rule[1].length; ++i) {
        var p = xpathTokenPrecedence(frame.rule[1][i]);
        ret = Math.max(ret, p);
      }
    }
  } else if (frame.tag) { /* TOKEN match */
    ret = xpathTokenPrecedence(frame.tag);

  } else if (frame.length) { /* Q_ match */
    for (var j = 0; j < frame.length; ++j) {
      var p = xpathGrammarPrecedence(frame[j]);
      ret = Math.max(ret, p);
    }
  }

  return ret;
}

function stackToString(stack) {
  var ret = '';
  for (var i = 0; i < stack.length; ++i) {
    if (ret) {
      ret += '\n';
    }
    ret += stack[i].tag.label;
  }
  return ret;
}


// XPath expression evaluation context. An XPath context consists of a
// DOM node, a list of DOM nodes that contains this node, a number
// that represents the position of the single node in the list, and a
// current set of variable bindings. (See XPath spec.)
//
// The interface of the expression context:
//
//   Constructor -- gets the node, its position, the node set it
//   belongs to, and a parent context as arguments. The parent context
//   is used to implement scoping rules for variables: if a variable
//   is not found in the current context, it is looked for in the
//   parent context, recursively. Except for node, all arguments have
//   default values: default position is 0, default node set is the
//   set that contains only the node, and the default parent is null.
//
//     Notice that position starts at 0 at the outside interface;
//     inside XPath expressions this shows up as position()=1.
//
//   clone() -- creates a new context with the current context as
//   parent. If passed as argument to clone(), the new context has a
//   different node, position, or node set. What is not passed is
//   inherited from the cloned context.
//
//   setVariable(name, expr) -- binds given XPath expression to the
//   name.
//
//   getVariable(name) -- what the name says.
//
//   setNode(position) -- sets the context to the node at the given
//   position. Needed to implement scoping rules for variables in
//   XPath. (A variable is visible to all subsequent siblings, not
//   only to its children.)

function ExprContext(node, opt_position, opt_nodelist, opt_parent) {
  this.node = node;
  this.position = opt_position || 0;
  this.nodelist = opt_nodelist || [ node ];
  this.variables = {};
  this.parent = opt_parent || null;
  if (opt_parent) {
    this.root = opt_parent.root;
  } else if (this.node.nodeType == DOM_DOCUMENT_NODE) {
    // NOTE(mesch): DOM Spec stipulates that the ownerDocument of a
    // document is null. Our root, however is the document that we are
    // processing, so the initial context is created from its document
    // node, which case we must handle here explcitly.
    this.root = node;
  } else {
    this.root = node.ownerDocument;
  }
}

ExprContext.prototype.clone = function(opt_node, opt_position, opt_nodelist) {
  return new ExprContext(
      opt_node || this.node,
      typeof opt_position != 'undefined' ? opt_position : this.position,
      opt_nodelist || this.nodelist, this);
};

ExprContext.prototype.setVariable = function(name, value) {
  this.variables[name] = value;
};

ExprContext.prototype.getVariable = function(name) {
  if (typeof this.variables[name] != 'undefined') {
    return this.variables[name];

  } else if (this.parent) {
    return this.parent.getVariable(name);

  } else {
    return null;
  }
};

ExprContext.prototype.setNode = function(position) {
  this.node = this.nodelist[position];
  this.position = position;
};

ExprContext.prototype.contextSize = function() {
  return this.nodelist.length;
};


// XPath expression values. They are what XPath expressions evaluate
// to. Strangely, the different value types are not specified in the
// XPath syntax, but only in the semantics, so they don't show up as
// nonterminals in the grammar. Yet, some expressions are required to
// evaluate to particular types, and not every type can be coerced
// into every other type. Although the types of XPath values are
// similar to the types present in JavaScript, the type coercion rules
// are a bit peculiar, so we explicitly model XPath types instead of
// mapping them onto JavaScript types. (See XPath spec.)
//
// The four types are:
//
//   StringValue
//
//   NumberValue
//
//   BooleanValue
//
//   NodeSetValue
//
// The common interface of the value classes consists of methods that
// implement the XPath type coercion rules:
//
//   stringValue() -- returns the value as a JavaScript String,
//
//   numberValue() -- returns the value as a JavaScript Number,
//
//   booleanValue() -- returns the value as a JavaScript Boolean,
//
//   nodeSetValue() -- returns the value as a JavaScript Array of DOM
//   Node objects.
//

function StringValue(value) {
  this.value = value;
  this.type = 'string';
}

StringValue.prototype.stringValue = function() {
  return this.value;
};

StringValue.prototype.booleanValue = function() {
  return this.value.length > 0;
};

StringValue.prototype.numberValue = function() {
  return this.value - 0;
};

StringValue.prototype.nodeSetValue = function() {
  throw this;
};

function BooleanValue(value) {
  this.value = value;
  this.type = 'boolean';
}

BooleanValue.prototype.stringValue = function() {
  return '' + this.value;
};

BooleanValue.prototype.booleanValue = function() {
  return this.value;
};

BooleanValue.prototype.numberValue = function() {
  return this.value ? 1 : 0;
};

BooleanValue.prototype.nodeSetValue = function() {
  throw this;
};

function NumberValue(value) {
  this.value = value;
  this.type = 'number';
}

NumberValue.prototype.stringValue = function() {
  return '' + this.value;
};

NumberValue.prototype.booleanValue = function() {
  return !!this.value;
};

NumberValue.prototype.numberValue = function() {
  return this.value - 0;
};

NumberValue.prototype.nodeSetValue = function() {
  throw this;
};

function NodeSetValue(value) {
  this.value = value;
  this.type = 'node-set';
}

NodeSetValue.prototype.stringValue = function() {
  if (this.value.length == 0) {
    return '';
  } else {
    return xmlValue(this.value[0]);
  }
};

NodeSetValue.prototype.booleanValue = function() {
  return this.value.length > 0;
};

NodeSetValue.prototype.numberValue = function() {
  return this.stringValue() - 0;
};

NodeSetValue.prototype.nodeSetValue = function() {
  return this.value;
};

// XPath expressions. They are used as nodes in the parse tree and
// possess an evaluate() method to compute an XPath value given an XPath
// context. Expressions are returned from the parser. Teh set of
// expression classes closely mirrors the set of non terminal symbols
// in the grammar. Every non trivial nonterminal symbol has a
// corresponding expression class.
//
// The common expression interface consists of the following methods:
//
// evaluate(context) -- evaluates the expression, returns a value.
//
// toString() -- returns the XPath text representation of the
// expression (defined in xsltdebug.js).
//
// parseTree(indent) -- returns a parse tree representation of the
// expression (defined in xsltdebug.js).

function TokenExpr(m) {
  this.value = m;
}

TokenExpr.prototype.evaluate = function() {
  return new StringValue(this.value);
};

function LocationExpr() {
  this.absolute = false;
  this.steps = [];
}

LocationExpr.prototype.appendStep = function(s) {
  this.steps.push(s);
};

LocationExpr.prototype.prependStep = function(s) {
  var steps0 = this.steps;
  this.steps = [ s ];
  for (var i = 0; i < steps0.length; ++i) {
    this.steps.push(steps0[i]);
  }
};

LocationExpr.prototype.evaluate = function(ctx) {
  var start;
  if (this.absolute) {
    start = ctx.root;

  } else {
    start = ctx.node;
  }

  var nodes = [];
  xPathStep(nodes, this.steps, 0, start, ctx);
  return new NodeSetValue(nodes);
};

function xPathStep(nodes, steps, step, input, ctx) {
  var s = steps[step];
  var ctx2 = ctx.clone(input);
  var nodelist = s.evaluate(ctx2).nodeSetValue();

  for (var i = 0; i < nodelist.length; ++i) {
    if (step == steps.length - 1) {
      nodes.push(nodelist[i]);
    } else {
      xPathStep(nodes, steps, step + 1, nodelist[i], ctx);
    }
  }
}

function StepExpr(axis, nodetest, opt_predicate) {
  this.axis = axis;
  this.nodetest = nodetest;
  this.predicate = opt_predicate || [];
}

StepExpr.prototype.appendPredicate = function(p) {
  this.predicate.push(p);
};

StepExpr.prototype.evaluate = function(ctx) {
  var input = ctx.node;
  var nodelist = [];

  // NOTE(mesch): When this was a switch() statement, it didn't work
  // in Safari/2.0. Not sure why though; it resulted in the JavaScript
  // console output "undefined" (without any line number or so).

  if (this.axis ==  xpathAxis.ANCESTOR_OR_SELF) {
    nodelist.push(input);
    for (var n = input.parentNode; n; n = n.parentNode) {
      nodelist.push(n);
    }

  } else if (this.axis == xpathAxis.ANCESTOR) {
    for (var n = input.parentNode; n; n = n.parentNode) {
      nodelist.push(n);
    }

  } else if (this.axis == xpathAxis.ATTRIBUTE) {
    copyArray(nodelist, input.attributes);

  } else if (this.axis == xpathAxis.CHILD) {
    copyArray(nodelist, input.childNodes);

  } else if (this.axis == xpathAxis.DESCENDANT_OR_SELF) {
    nodelist.push(input);
    xpathCollectDescendants(nodelist, input);

  } else if (this.axis == xpathAxis.DESCENDANT) {
    xpathCollectDescendants(nodelist, input);

  } else if (this.axis == xpathAxis.FOLLOWING) {
    for (var n = input; n; n = n.parentNode) {
      for (var nn = n.nextSibling; nn; nn = nn.nextSibling) {
        nodelist.push(nn);
        xpathCollectDescendants(nodelist, nn);
      }
    }

  } else if (this.axis == xpathAxis.FOLLOWING_SIBLING) {
    for (var n = input.nextSibling; n; n = n.nextSibling) {
      nodelist.push(n);
    }

  } else if (this.axis == xpathAxis.NAMESPACE) {
    alert('not implemented: axis namespace');

  } else if (this.axis == xpathAxis.PARENT) {
    if (input.parentNode) {
      nodelist.push(input.parentNode);
    }

  } else if (this.axis == xpathAxis.PRECEDING) {
    for (var n = input; n; n = n.parentNode) {
      for (var nn = n.previousSibling; nn; nn = nn.previousSibling) {
        nodelist.push(nn);
        xpathCollectDescendantsReverse(nodelist, nn);
      }
    }

  } else if (this.axis == xpathAxis.PRECEDING_SIBLING) {
    for (var n = input.previousSibling; n; n = n.previousSibling) {
      nodelist.push(n);
    }

  } else if (this.axis == xpathAxis.SELF) {
    nodelist.push(input);

  } else {
    throw 'ERROR -- NO SUCH AXIS: ' + this.axis;
  }

  // process node test
  var nodelist0 = nodelist;
  nodelist = [];
  for (var i = 0; i < nodelist0.length; ++i) {
    var n = nodelist0[i];
    if (this.nodetest.evaluate(ctx.clone(n, i, nodelist0)).booleanValue()) {
      nodelist.push(n);
    }
  }

  // process predicates
  for (var i = 0; i < this.predicate.length; ++i) {
    var nodelist0 = nodelist;
    nodelist = [];
    for (var ii = 0; ii < nodelist0.length; ++ii) {
      var n = nodelist0[ii];
      if (this.predicate[i].evaluate(ctx.clone(n, ii, nodelist0)).booleanValue()) {
        nodelist.push(n);
      }
    }
  }

  return new NodeSetValue(nodelist);
};

function NodeTestAny() {
  this.value = new BooleanValue(true);
}

NodeTestAny.prototype.evaluate = function(ctx) {
  return this.value;
};

function NodeTestElementOrAttribute() {}

NodeTestElementOrAttribute.prototype.evaluate = function(ctx) {
  return new BooleanValue(
      ctx.node.nodeType == DOM_ELEMENT_NODE ||
      ctx.node.nodeType == DOM_ATTRIBUTE_NODE);
};

function NodeTestText() {}

NodeTestText.prototype.evaluate = function(ctx) {
  return new BooleanValue(ctx.node.nodeType == DOM_TEXT_NODE);
};

function NodeTestComment() {}

NodeTestComment.prototype.evaluate = function(ctx) {
  return new BooleanValue(ctx.node.nodeType == DOM_COMMENT_NODE);
};

function NodeTestPI(target) {
  this.target = target;
}

NodeTestPI.prototype.evaluate = function(ctx) {
  return new
  BooleanValue(ctx.node.nodeType == DOM_PROCESSING_INSTRUCTION_NODE &&
               (!this.target || ctx.node.nodeName == this.target));
};

function NodeTestNC(nsprefix) {
  this.regex = new RegExp("^" + nsprefix + ":");
  this.nsprefix = nsprefix;
}

NodeTestNC.prototype.evaluate = function(ctx) {
  var n = ctx.node;
  return new BooleanValue(this.regex.match(n.nodeName));
};

function NodeTestName(name) {
  this.name = name;
}

NodeTestName.prototype.evaluate = function(ctx) {
  var n = ctx.node;
  return new BooleanValue(n.nodeName == this.name);
};

function PredicateExpr(expr) {
  this.expr = expr;
}

PredicateExpr.prototype.evaluate = function(ctx) {
  var v = this.expr.evaluate(ctx);
  if (v.type == 'number') {
    // NOTE(mesch): Internally, position is represented starting with
    // 0, however in XPath position starts with 1. See functions
    // position() and last().
    return new BooleanValue(ctx.position == v.numberValue() - 1);
  } else {
    return new BooleanValue(v.booleanValue());
  }
};

function FunctionCallExpr(name) {
  this.name = name;
  this.args = [];
}

FunctionCallExpr.prototype.appendArg = function(arg) {
  this.args.push(arg);
};

FunctionCallExpr.prototype.evaluate = function(ctx) {
  var fn = '' + this.name.value;
  var f = this.xpathfunctions[fn];
  if (f) {
    return f.call(this, ctx);
  } else {
    xpathLog('XPath NO SUCH FUNCTION ' + fn);
    return new BooleanValue(false);
  }
};

FunctionCallExpr.prototype.xpathfunctions = {
  'last': function(ctx) {
    assert(this.args.length == 0);
    // NOTE(mesch): XPath position starts at 1.
    return new NumberValue(ctx.contextSize());
  },

  'position': function(ctx) {
    assert(this.args.length == 0);
    // NOTE(mesch): XPath position starts at 1.
    return new NumberValue(ctx.position + 1);
  },

  'count': function(ctx) {
    assert(this.args.length == 1);
    var v = this.args[0].evaluate(ctx);
    return new NumberValue(v.nodeSetValue().length);
  },

  'id': function(ctx) {
    assert(this.args.length == 1);
    var e = this.args[0].evaluate(ctx);
    var ret = [];
    var ids;
    if (e.type == 'node-set') {
      ids = [];
      var en = e.nodeSetValue();
      for (var i = 0; i < en.length; ++i) {
        var v = xmlValue(en[i]).split(/\s+/);
        for (var ii = 0; ii < v.length; ++ii) {
          ids.push(v[ii]);
        }
      }
    } else {
      ids = e.stringValue().split(/\s+/);
    }
    var d = ctx.node.ownerDocument;
    for (var i = 0; i < ids.length; ++i) {
      var n = d.getElementById(ids[i]);
      if (n) {
        ret.push(n);
      }
    }
    return new NodeSetValue(ret);
  },

  'local-name': function(ctx) {
    alert('not implmented yet: XPath function local-name()');
  },

  'namespace-uri': function(ctx) {
    alert('not implmented yet: XPath function namespace-uri()');
  },

  'name': function(ctx) {
    assert(this.args.length == 1 || this.args.length == 0);
    var n;
    if (this.args.length == 0) {
      n = [ ctx.node ];
    } else {
      n = this.args[0].evaluate(ctx).nodeSetValue();
    }

    if (n.length == 0) {
      return new StringValue('');
    } else {
      return new StringValue(n[0].nodeName);
    }
  },

  'string':  function(ctx) {
    assert(this.args.length == 1 || this.args.length == 0);
    if (this.args.length == 0) {
      return new StringValue(new NodeSetValue([ ctx.node ]).stringValue());
    } else {
      return new StringValue(this.args[0].evaluate(ctx).stringValue());
    }
  },

  'concat': function(ctx) {
    var ret = '';
    for (var i = 0; i < this.args.length; ++i) {
      ret += this.args[i].evaluate(ctx).stringValue();
    }
    return new StringValue(ret);
  },

  'starts-with': function(ctx) {
    assert(this.args.length == 2);
    var s0 = this.args[0].evaluate(ctx).stringValue();
    var s1 = this.args[1].evaluate(ctx).stringValue();
    return new BooleanValue(s0.indexOf(s1) == 0);
  },

  'contains': function(ctx) {
    assert(this.args.length == 2);
    var s0 = this.args[0].evaluate(ctx).stringValue();
    var s1 = this.args[1].evaluate(ctx).stringValue();
    return new BooleanValue(s0.indexOf(s1) != -1);
  },

  'substring-before': function(ctx) {
    assert(this.args.length == 2);
    var s0 = this.args[0].evaluate(ctx).stringValue();
    var s1 = this.args[1].evaluate(ctx).stringValue();
    var i = s0.indexOf(s1);
    var ret;
    if (i == -1) {
      ret = '';
    } else {
      ret = s0.substr(0,i);
    }
    return new StringValue(ret);
  },

  'substring-after': function(ctx) {
    assert(this.args.length == 2);
    var s0 = this.args[0].evaluate(ctx).stringValue();
    var s1 = this.args[1].evaluate(ctx).stringValue();
    var i = s0.indexOf(s1);
    var ret;
    if (i == -1) {
      ret = '';
    } else {
      ret = s0.substr(i + s1.length);
    }
    return new StringValue(ret);
  },

  'substring': function(ctx) {
    // NOTE: XPath defines the position of the first character in a
    // string to be 1, in JavaScript this is 0 ([XPATH] Section 4.2).
    assert(this.args.length == 2 || this.args.length == 3);
    var s0 = this.args[0].evaluate(ctx).stringValue();
    var s1 = this.args[1].evaluate(ctx).numberValue();
    var ret;
    if (this.args.length == 2) {
      var i1 = Math.max(0, Math.round(s1) - 1);
      ret = s0.substr(i1);

    } else {
      var s2 = this.args[2].evaluate(ctx).numberValue();
      var i0 = Math.round(s1) - 1;
      var i1 = Math.max(0, i0);
      var i2 = Math.round(s2) - Math.max(0, -i0);
      ret = s0.substr(i1, i2);
    }
    return new StringValue(ret);
  },

  'string-length': function(ctx) {
    var s;
    if (this.args.length > 0) {
      s = this.args[0].evaluate(ctx).stringValue();
    } else {
      s = new NodeSetValue([ ctx.node ]).stringValue();
    }
    return new NumberValue(s.length);
  },

  'normalize-space': function(ctx) {
    var s;
    if (this.args.length > 0) {
      s = this.args[0].evaluate(ctx).stringValue();
    } else {
      s = new NodeSetValue([ ctx.node ]).stringValue();
    }
    s = s.replace(/^\s*/,'').replace(/\s*$/,'').replace(/\s+/g, ' ');
    return new StringValue(s);
  },

  'translate': function(ctx) {
    assert(this.args.length == 3);
    var s0 = this.args[0].evaluate(ctx).stringValue();
    var s1 = this.args[1].evaluate(ctx).stringValue();
    var s2 = this.args[2].evaluate(ctx).stringValue();

    for (var i = 0; i < s1.length; ++i) {
      s0 = s0.replace(new RegExp(s1.charAt(i), 'g'), s2.charAt(i));
    }
    return new StringValue(s0);
  },

  'boolean': function(ctx) {
    assert(this.args.length == 1);
    return new BooleanValue(this.args[0].evaluate(ctx).booleanValue());
  },

  'not': function(ctx) {
    assert(this.args.length == 1);
    var ret = !this.args[0].evaluate(ctx).booleanValue();
    return new BooleanValue(ret);
  },

  'true': function(ctx) {
    assert(this.args.length == 0);
    return new BooleanValue(true);
  },

  'false': function(ctx) {
    assert(this.args.length == 0);
    return new BooleanValue(false);
  },

  'lang': function(ctx) {
    assert(this.args.length == 1);
    var lang = this.args[0].evaluate(ctx).stringValue();
    var xmllang;
    var n = ctx.node;
    while (n && n != n.parentNode /* just in case ... */) {
      xmllang = n.getAttribute('xml:lang');
      if (xmllang) {
        break;
      }
      n = n.parentNode;
    }
    if (!xmllang) {
      return new BooleanValue(false);
    } else {
      var re = new RegExp('^' + lang + '$', 'i');
      return new BooleanValue(xmllang.match(re) ||
                              xmllang.replace(/_.*$/,'').match(re));
    }
  },

  'number': function(ctx) {
    assert(this.args.length == 1 || this.args.length == 0);

    if (this.args.length == 1) {
      return new NumberValue(this.args[0].evaluate(ctx).numberValue());
    } else {
      return new NumberValue(new NodeSetValue([ ctx.node ]).numberValue());
    }
  },

  'sum': function(ctx) {
    assert(this.args.length == 1);
    var n = this.args[0].evaluate(ctx).nodeSetValue();
    var sum = 0;
    for (var i = 0; i < n.length; ++i) {
      sum += xmlValue(n[i]) - 0;
    }
    return new NumberValue(sum);
  },

  'floor': function(ctx) {
    assert(this.args.length == 1);
    var num = this.args[0].evaluate(ctx).numberValue();
    return new NumberValue(Math.floor(num));
  },

  'ceiling': function(ctx) {
    assert(this.args.length == 1);
    var num = this.args[0].evaluate(ctx).numberValue();
    return new NumberValue(Math.ceil(num));
  },

  'round': function(ctx) {
    assert(this.args.length == 1);
    var num = this.args[0].evaluate(ctx).numberValue();
    return new NumberValue(Math.round(num));
  },

  // TODO(mesch): The following functions are custom. There is a
  // standard that defines how to add functions, which should be
  // applied here.

  'ext-join': function(ctx) {
    assert(this.args.length == 2);
    var nodes = this.args[0].evaluate(ctx).nodeSetValue();
    var delim = this.args[1].evaluate(ctx).stringValue();
    var ret = '';
    for (var i = 0; i < nodes.length; ++i) {
      if (ret) {
        ret += delim;
      }
      ret += xmlValue(nodes[i]);
    }
    return new StringValue(ret);
  },

  // ext-if() evaluates and returns its second argument, if the
  // boolean value of its first argument is true, otherwise it
  // evaluates and returns its third argument.

  'ext-if': function(ctx) {
    assert(this.args.length == 3);
    if (this.args[0].evaluate(ctx).booleanValue()) {
      return this.args[1].evaluate(ctx);
    } else {
      return this.args[2].evaluate(ctx);
    }
  },

  // ext-cardinal() evaluates its single argument as a number, and
  // returns the current node that many times. It can be used in the
  // select attribute to iterate over an integer range.

  'ext-cardinal': function(ctx) {
    assert(this.args.length >= 1);
    var c = this.args[0].evaluate(ctx).numberValue();
    var ret = [];
    for (var i = 0; i < c; ++i) {
      ret.push(ctx.node);
    }
    return new NodeSetValue(ret);
  }
};

function UnionExpr(expr1, expr2) {
  this.expr1 = expr1;
  this.expr2 = expr2;
}

UnionExpr.prototype.evaluate = function(ctx) {
  var nodes1 = this.expr1.evaluate(ctx).nodeSetValue();
  var nodes2 = this.expr2.evaluate(ctx).nodeSetValue();
  var I1 = nodes1.length;
  for (var i2 = 0; i2 < nodes2.length; ++i2) {
    var n = nodes2[i2];
    var inBoth = false;
    for (var i1 = 0; i1 < I1; ++i1) {
      if (nodes1[i1] == n) {
        inBoth = true;
        i1 = I1; // break inner loop
      }
    }
    if (!inBoth) {
      nodes1.push(n);
    }
  }
  return new NodeSetValue(nodes1);
};

function PathExpr(filter, rel) {
  this.filter = filter;
  this.rel = rel;
}

PathExpr.prototype.evaluate = function(ctx) {
  var nodes = this.filter.evaluate(ctx).nodeSetValue();
  var nodes1 = [];
  for (var i = 0; i < nodes.length; ++i) {
    var nodes0 = this.rel.evaluate(ctx.clone(nodes[i], i, nodes)).nodeSetValue();
    for (var ii = 0; ii < nodes0.length; ++ii) {
      nodes1.push(nodes0[ii]);
    }
  }
  return new NodeSetValue(nodes1);
};

function FilterExpr(expr, predicate) {
  this.expr = expr;
  this.predicate = predicate;
}

FilterExpr.prototype.evaluate = function(ctx) {
  var nodes = this.expr.evaluate(ctx).nodeSetValue();
  for (var i = 0; i < this.predicate.length; ++i) {
    var nodes0 = nodes;
    nodes = [];
    for (var j = 0; j < nodes0.length; ++j) {
      var n = nodes0[j];
      if (this.predicate[i].evaluate(ctx.clone(n, j, nodes0)).booleanValue()) {
        nodes.push(n);
      }
    }
  }

  return new NodeSetValue(nodes);
};

function UnaryMinusExpr(expr) {
  this.expr = expr;
}

UnaryMinusExpr.prototype.evaluate = function(ctx) {
  return new NumberValue(-this.expr.evaluate(ctx).numberValue());
};

function BinaryExpr(expr1, op, expr2) {
  this.expr1 = expr1;
  this.expr2 = expr2;


  this.op = op;
}

BinaryExpr.prototype.evaluate = function(ctx) {
  var ret;
  switch (this.op.value) {
    case 'or':
      ret = new BooleanValue(this.expr1.evaluate(ctx).booleanValue() ||
                             this.expr2.evaluate(ctx).booleanValue());
      break;

    case 'and':
      ret = new BooleanValue(this.expr1.evaluate(ctx).booleanValue() &&
                             this.expr2.evaluate(ctx).booleanValue());
      break;

    case '+':
      ret = new NumberValue(this.expr1.evaluate(ctx).numberValue() +
                            this.expr2.evaluate(ctx).numberValue());
      break;

    case '-':
      ret = new NumberValue(this.expr1.evaluate(ctx).numberValue() -
                            this.expr2.evaluate(ctx).numberValue());
      break;

    case '*':
      ret = new NumberValue(this.expr1.evaluate(ctx).numberValue() *
                            this.expr2.evaluate(ctx).numberValue());
      break;

    case 'mod':
      ret = new NumberValue(this.expr1.evaluate(ctx).numberValue() %
                            this.expr2.evaluate(ctx).numberValue());
      break;

    case 'div':
      ret = new NumberValue(this.expr1.evaluate(ctx).numberValue() /
                            this.expr2.evaluate(ctx).numberValue());
      break;

    case '=':
      ret = this.compare(ctx, function(x1, x2) { return x1 == x2; });
      break;

    case '!=':
      ret = this.compare(ctx, function(x1, x2) { return x1 != x2; });
      break;

    case '<':
      ret = this.compare(ctx, function(x1, x2) { return x1 < x2; });
      break;

    case '<=':
      ret = this.compare(ctx, function(x1, x2) { return x1 <= x2; });
      break;

    case '>':
      ret = this.compare(ctx, function(x1, x2) { return x1 > x2; });
      break;

    case '>=':
      ret = this.compare(ctx, function(x1, x2) { return x1 >= x2; });
      break;

    default:
      alert('BinaryExpr.evaluate: ' + this.op.value);
  }
  return ret;
};

BinaryExpr.prototype.compare = function(ctx, cmp) {
  var v1 = this.expr1.evaluate(ctx);
  var v2 = this.expr2.evaluate(ctx);

  var ret;
  if (v1.type == 'node-set' && v2.type == 'node-set') {
    var n1 = v1.nodeSetValue();
    var n2 = v2.nodeSetValue();
    ret = false;
    for (var i1 = 0; i1 < n1.length; ++i1) {
      for (var i2 = 0; i2 < n2.length; ++i2) {
        if (cmp(xmlValue(n1[i1]), xmlValue(n2[i2]))) {
          ret = true;
          // Break outer loop. Labels confuse the jscompiler and we
          // don't use them.
          i2 = n2.length;
          i1 = n1.length;
        }
      }
    }

  } else if (v1.type == 'node-set' || v2.type == 'node-set') {

    if (v1.type == 'number') {
      var s = v1.numberValue();
      var n = v2.nodeSetValue();

      ret = false;
      for (var i = 0;  i < n.length; ++i) {
        var nn = xmlValue(n[i]) - 0;
        if (cmp(s, nn)) {
          ret = true;
          break;
        }
      }

    } else if (v2.type == 'number') {
      var n = v1.nodeSetValue();
      var s = v2.numberValue();

      ret = false;
      for (var i = 0;  i < n.length; ++i) {
        var nn = xmlValue(n[i]) - 0;
        if (cmp(nn, s)) {
          ret = true;
          break;
        }
      }

    } else if (v1.type == 'string') {
      var s = v1.stringValue();
      var n = v2.nodeSetValue();

      ret = false;
      for (var i = 0;  i < n.length; ++i) {
        var nn = xmlValue(n[i]);
        if (cmp(s, nn)) {
          ret = true;
          break;
        }
      }

    } else if (v2.type == 'string') {
      var n = v1.nodeSetValue();
      var s = v2.stringValue();

      ret = false;
      for (var i = 0;  i < n.length; ++i) {
        var nn = xmlValue(n[i]);
        if (cmp(nn, s)) {
          ret = true;
          break;
        }
      }

    } else {
      ret = cmp(v1.booleanValue(), v2.booleanValue());
    }

  } else if (v1.type == 'boolean' || v2.type == 'boolean') {
    ret = cmp(v1.booleanValue(), v2.booleanValue());

  } else if (v1.type == 'number' || v2.type == 'number') {
    ret = cmp(v1.numberValue(), v2.numberValue());

  } else {
    ret = cmp(v1.stringValue(), v2.stringValue());
  }

  return new BooleanValue(ret);
};

function LiteralExpr(value) {
  this.value = value;
}

LiteralExpr.prototype.evaluate = function(ctx) {
  return new StringValue(this.value);
};

function NumberExpr(value) {
  this.value = value;
}

NumberExpr.prototype.evaluate = function(ctx) {
  return new NumberValue(this.value);
};

function VariableExpr(name) {
  this.name = name;
}

VariableExpr.prototype.evaluate = function(ctx) {
  return ctx.getVariable(this.name);
};

// Factory functions for semantic values (i.e. Expressions) of the
// productions in the grammar. When a production is matched to reduce
// the current parse state stack, the function is called with the
// semantic values of the matched elements as arguments, and returns
// another semantic value. The semantic value is a node of the parse
// tree, an expression object with an evaluate() method that evaluates the
// expression in an actual context. These factory functions are used
// in the specification of the grammar rules, below.

function makeTokenExpr(m) {
  return new TokenExpr(m);
}

function passExpr(e) {
  return e;
}

function makeLocationExpr1(slash, rel) {
  rel.absolute = true;
  return rel;
}

function makeLocationExpr2(dslash, rel) {
  rel.absolute = true;
  rel.prependStep(makeAbbrevStep(dslash.value));
  return rel;
}

function makeLocationExpr3(slash) {
  var ret = new LocationExpr();
  ret.appendStep(makeAbbrevStep('.'));
  ret.absolute = true;
  return ret;
}

function makeLocationExpr4(dslash) {
  var ret = new LocationExpr();
  ret.absolute = true;
  ret.appendStep(makeAbbrevStep(dslash.value));
  return ret;
}

function makeLocationExpr5(step) {
  var ret = new LocationExpr();
  ret.appendStep(step);
  return ret;
}

function makeLocationExpr6(rel, slash, step) {
  rel.appendStep(step);
  return rel;
}

function makeLocationExpr7(rel, dslash, step) {
  rel.appendStep(makeAbbrevStep(dslash.value));
  return rel;
}

function makeStepExpr1(dot) {
  return makeAbbrevStep(dot.value);
}

function makeStepExpr2(ddot) {
  return makeAbbrevStep(ddot.value);
}

function makeStepExpr3(axisname, axis, nodetest) {
  return new StepExpr(axisname.value, nodetest);
}

function makeStepExpr4(at, nodetest) {
  return new StepExpr('attribute', nodetest);
}

function makeStepExpr5(nodetest) {
  return new StepExpr('child', nodetest);
}

function makeStepExpr6(step, predicate) {
  step.appendPredicate(predicate);
  return step;
}

function makeAbbrevStep(abbrev) {
  switch (abbrev) {
  case '//':
    return new StepExpr('descendant-or-self', new NodeTestAny);

  case '.':
    return new StepExpr('self', new NodeTestAny);

  case '..':
    return new StepExpr('parent', new NodeTestAny);
  }
}

function makeNodeTestExpr1(asterisk) {
  return new NodeTestElementOrAttribute;
}

function makeNodeTestExpr2(ncname, colon, asterisk) {
  return new NodeTestNC(ncname.value);
}

function makeNodeTestExpr3(qname) {
  return new NodeTestName(qname.value);
}

function makeNodeTestExpr4(typeo, parenc) {
  var type = typeo.value.replace(/\s*\($/, '');
  switch(type) {
  case 'node':
    return new NodeTestAny;

  case 'text':
    return new NodeTestText;

  case 'comment':
    return new NodeTestComment;

  case 'processing-instruction':
    return new NodeTestPI('');
  }
}

function makeNodeTestExpr5(typeo, target, parenc) {
  var type = typeo.replace(/\s*\($/, '');
  if (type != 'processing-instruction') {
    throw type;
  }
  return new NodeTestPI(target.value);
}

function makePredicateExpr(pareno, expr, parenc) {
  return new PredicateExpr(expr);
}

function makePrimaryExpr(pareno, expr, parenc) {
  return expr;
}

function makeFunctionCallExpr1(name, pareno, parenc) {
  return new FunctionCallExpr(name);
}

function makeFunctionCallExpr2(name, pareno, arg1, args, parenc) {
  var ret = new FunctionCallExpr(name);
  ret.appendArg(arg1);
  for (var i = 0; i < args.length; ++i) {
    ret.appendArg(args[i]);
  }
  return ret;
}

function makeArgumentExpr(comma, expr) {
  return expr;
}

function makeUnionExpr(expr1, pipe, expr2) {
  return new UnionExpr(expr1, expr2);
}

function makePathExpr1(filter, slash, rel) {
  return new PathExpr(filter, rel);
}

function makePathExpr2(filter, dslash, rel) {
  rel.prependStep(makeAbbrevStep(dslash.value));
  return new PathExpr(filter, rel);
}

function makeFilterExpr(expr, predicates) {
  if (predicates.length > 0) {
    return new FilterExpr(expr, predicates);
  } else {
    return expr;
  }
}

function makeUnaryMinusExpr(minus, expr) {
  return new UnaryMinusExpr(expr);
}

function makeBinaryExpr(expr1, op, expr2) {
  return new BinaryExpr(expr1, op, expr2);
}

function makeLiteralExpr(token) {
  // remove quotes from the parsed value:
  var value = token.value.substring(1, token.value.length - 1);
  return new LiteralExpr(value);
}

function makeNumberExpr(token) {
  return new NumberExpr(token.value);
}

function makeVariableReference(dollar, name) {
  return new VariableExpr(name.value);
}

// Used before parsing for optimization of common simple cases. See
// the begin of xpathParse() for which they are.
function makeSimpleExpr(expr) {
  if (expr.charAt(0) == '$') {
    return new VariableExpr(expr.substr(1));
  } else if (expr.charAt(0) == '@') {
    var a = new NodeTestName(expr.substr(1));
    var b = new StepExpr('attribute', a);
    var c = new LocationExpr();
    c.appendStep(b);
    return c;
  } else if (expr.match(/^[0-9]+$/)) {
    return new NumberExpr(expr);
  } else {
    var a = new NodeTestName(expr);
    var b = new StepExpr('child', a);
    var c = new LocationExpr();
    c.appendStep(b);
    return c;
  }
}

function makeSimpleExpr2(expr) {
  var steps = stringSplit(expr, '/');
  var c = new LocationExpr();
  for (var i = 0; i < steps.length; ++i) {
    var a = new NodeTestName(steps[i]);
    var b = new StepExpr('child', a);
    c.appendStep(b);
  }
  return c;
}

// The axes of XPath expressions.

var xpathAxis = {
  ANCESTOR_OR_SELF: 'ancestor-or-self',
  ANCESTOR: 'ancestor',
  ATTRIBUTE: 'attribute',
  CHILD: 'child',
  DESCENDANT_OR_SELF: 'descendant-or-self',
  DESCENDANT: 'descendant',
  FOLLOWING_SIBLING: 'following-sibling',
  FOLLOWING: 'following',
  NAMESPACE: 'namespace',
  PARENT: 'parent',
  PRECEDING_SIBLING: 'preceding-sibling',
  PRECEDING: 'preceding',
  SELF: 'self'
};

var xpathAxesRe = [
    xpathAxis.ANCESTOR_OR_SELF,
    xpathAxis.ANCESTOR,
    xpathAxis.ATTRIBUTE,
    xpathAxis.CHILD,
    xpathAxis.DESCENDANT_OR_SELF,
    xpathAxis.DESCENDANT,
    xpathAxis.FOLLOWING_SIBLING,
    xpathAxis.FOLLOWING,
    xpathAxis.NAMESPACE,
    xpathAxis.PARENT,
    xpathAxis.PRECEDING_SIBLING,
    xpathAxis.PRECEDING,
    xpathAxis.SELF
].join('|');


// The tokens of the language. The label property is just used for
// generating debug output. The prec property is the precedence used
// for shift/reduce resolution. Default precedence is 0 as a lookahead
// token and 2 on the stack. TODO(mesch): this is certainly not
// necessary and too complicated. Simplify this!

// NOTE: tabular formatting is the big exception, but here it should
// be OK.

var TOK_PIPE =   { label: "|",   prec:   17, re: new RegExp("^\\|") };
var TOK_DSLASH = { label: "//",  prec:   19, re: new RegExp("^//")  };
var TOK_SLASH =  { label: "/",   prec:   30, re: new RegExp("^/")   };
var TOK_AXIS =   { label: "::",  prec:   20, re: new RegExp("^::")  };
var TOK_COLON =  { label: ":",   prec: 1000, re: new RegExp("^:")  };
var TOK_AXISNAME = { label: "[axis]", re: new RegExp('^(' + xpathAxesRe + ')') };
var TOK_PARENO = { label: "(",   prec:   34, re: new RegExp("^\\(") };
var TOK_PARENC = { label: ")",               re: new RegExp("^\\)") };
var TOK_DDOT =   { label: "..",  prec:   34, re: new RegExp("^\\.\\.") };
var TOK_DOT =    { label: ".",   prec:   34, re: new RegExp("^\\.") };
var TOK_AT =     { label: "@",   prec:   34, re: new RegExp("^@")   };

var TOK_COMMA =  { label: ",",               re: new RegExp("^,") };

var TOK_OR =     { label: "or",  prec:   10, re: new RegExp("^or\\b") };
var TOK_AND =    { label: "and", prec:   11, re: new RegExp("^and\\b") };
var TOK_EQ =     { label: "=",   prec:   12, re: new RegExp("^=")   };
var TOK_NEQ =    { label: "!=",  prec:   12, re: new RegExp("^!=")  };
var TOK_GE =     { label: ">=",  prec:   13, re: new RegExp("^>=")  };
var TOK_GT =     { label: ">",   prec:   13, re: new RegExp("^>")   };
var TOK_LE =     { label: "<=",  prec:   13, re: new RegExp("^<=")  };
var TOK_LT =     { label: "<",   prec:   13, re: new RegExp("^<")   };
var TOK_PLUS =   { label: "+",   prec:   14, re: new RegExp("^\\+"), left: true };
var TOK_MINUS =  { label: "-",   prec:   14, re: new RegExp("^\\-"), left: true };
var TOK_DIV =    { label: "div", prec:   15, re: new RegExp("^div\\b"), left: true };
var TOK_MOD =    { label: "mod", prec:   15, re: new RegExp("^mod\\b"), left: true };

var TOK_BRACKO = { label: "[",   prec:   32, re: new RegExp("^\\[") };
var TOK_BRACKC = { label: "]",               re: new RegExp("^\\]") };
var TOK_DOLLAR = { label: "$",               re: new RegExp("^\\$") };

var TOK_NCNAME = { label: "[ncname]", re: new RegExp('^' + XML_NC_NAME) };

var TOK_ASTERISK = { label: "*", prec: 15, re: new RegExp("^\\*"), left: true };
var TOK_LITERALQ = { label: "[litq]", prec: 20, re: new RegExp("^'[^\\']*'") };
var TOK_LITERALQQ = {
  label: "[litqq]",
  prec: 20,
  re: new RegExp('^"[^\\"]*"')
};

var TOK_NUMBER  = {
  label: "[number]",
  prec: 35,
  re: new RegExp('^\\d+(\\.\\d*)?') };

var TOK_QNAME = {
  label: "[qname]",
  re: new RegExp('^(' + XML_NC_NAME + ':)?' + XML_NC_NAME)
};

var TOK_NODEO = {
  label: "[nodetest-start]",
  re: new RegExp('^(processing-instruction|comment|text|node)\\(')
};

// The table of the tokens of our grammar, used by the lexer: first
// column the tag, second column a regexp to recognize it in the
// input, third column the precedence of the token, fourth column a
// factory function for the semantic value of the token.
//
// NOTE: order of this list is important, because the first match
// counts. Cf. DDOT and DOT, and AXIS and COLON.

var xpathTokenRules = [
    TOK_DSLASH,
    TOK_SLASH,
    TOK_DDOT,
    TOK_DOT,
    TOK_AXIS,
    TOK_COLON,
    TOK_AXISNAME,
    TOK_NODEO,
    TOK_PARENO,
    TOK_PARENC,
    TOK_BRACKO,
    TOK_BRACKC,
    TOK_AT,
    TOK_COMMA,
    TOK_OR,
    TOK_AND,
    TOK_NEQ,
    TOK_EQ,
    TOK_GE,
    TOK_GT,
    TOK_LE,
    TOK_LT,
    TOK_PLUS,
    TOK_MINUS,
    TOK_ASTERISK,
    TOK_PIPE,
    TOK_MOD,
    TOK_DIV,
    TOK_LITERALQ,
    TOK_LITERALQQ,
    TOK_NUMBER,
    TOK_QNAME,
    TOK_NCNAME,
    TOK_DOLLAR
];

// All the nonterminals of the grammar. The nonterminal objects are
// identified by object identity; the labels are used in the debug
// output only.
var XPathLocationPath = { label: "LocationPath" };
var XPathRelativeLocationPath = { label: "RelativeLocationPath" };
var XPathAbsoluteLocationPath = { label: "AbsoluteLocationPath" };
var XPathStep = { label: "Step" };
var XPathNodeTest = { label: "NodeTest" };
var XPathPredicate = { label: "Predicate" };
var XPathLiteral = { label: "Literal" };
var XPathExpr = { label: "Expr" };
var XPathPrimaryExpr = { label: "PrimaryExpr" };
var XPathVariableReference = { label: "Variablereference" };
var XPathNumber = { label: "Number" };
var XPathFunctionCall = { label: "FunctionCall" };
var XPathArgumentRemainder = { label: "ArgumentRemainder" };
var XPathPathExpr = { label: "PathExpr" };
var XPathUnionExpr = { label: "UnionExpr" };
var XPathFilterExpr = { label: "FilterExpr" };
var XPathDigits = { label: "Digits" };

var xpathNonTerminals = [
    XPathLocationPath,
    XPathRelativeLocationPath,
    XPathAbsoluteLocationPath,
    XPathStep,
    XPathNodeTest,
    XPathPredicate,
    XPathLiteral,
    XPathExpr,
    XPathPrimaryExpr,
    XPathVariableReference,
    XPathNumber,
    XPathFunctionCall,
    XPathArgumentRemainder,
    XPathPathExpr,
    XPathUnionExpr,
    XPathFilterExpr,
    XPathDigits
];

// Quantifiers that are used in the productions of the grammar.
var Q_01 = { label: "?" };
var Q_MM = { label: "*" };
var Q_1M = { label: "+" };

// Tag for left associativity (right assoc is implied by undefined).
var ASSOC_LEFT = true;

// The productions of the grammar. Columns of the table:
//
// - target nonterminal,
// - pattern,
// - precedence,
// - semantic value factory
//
// The semantic value factory is a function that receives parse tree
// nodes from the stack frames of the matched symbols as arguments and
// returns an a node of the parse tree. The node is stored in the top
// stack frame along with the target object of the rule. The node in
// the parse tree is an expression object that has an evaluate() method
// and thus evaluates XPath expressions.
//
// The precedence is used to decide between reducing and shifting by
// comparing the precendence of the rule that is candidate for
// reducing with the precedence of the look ahead token. Precedence of
// -1 means that the precedence of the tokens in the pattern is used
// instead. TODO: It shouldn't be necessary to explicitly assign
// precedences to rules.

var xpathGrammarRules =
  [
   [ XPathLocationPath, [ XPathRelativeLocationPath ], 18,
     passExpr ],
   [ XPathLocationPath, [ XPathAbsoluteLocationPath ], 18,
     passExpr ],

   [ XPathAbsoluteLocationPath, [ TOK_SLASH, XPathRelativeLocationPath ], 18,
     makeLocationExpr1 ],
   [ XPathAbsoluteLocationPath, [ TOK_DSLASH, XPathRelativeLocationPath ], 18,
     makeLocationExpr2 ],

   [ XPathAbsoluteLocationPath, [ TOK_SLASH ], 0,
     makeLocationExpr3 ],
   [ XPathAbsoluteLocationPath, [ TOK_DSLASH ], 0,
     makeLocationExpr4 ],

   [ XPathRelativeLocationPath, [ XPathStep ], 31,
     makeLocationExpr5 ],
   [ XPathRelativeLocationPath,
     [ XPathRelativeLocationPath, TOK_SLASH, XPathStep ], 31,
     makeLocationExpr6 ],
   [ XPathRelativeLocationPath,
     [ XPathRelativeLocationPath, TOK_DSLASH, XPathStep ], 31,
     makeLocationExpr7 ],

   [ XPathStep, [ TOK_DOT ], 33,
     makeStepExpr1 ],
   [ XPathStep, [ TOK_DDOT ], 33,
     makeStepExpr2 ],
   [ XPathStep,
     [ TOK_AXISNAME, TOK_AXIS, XPathNodeTest ], 33,
     makeStepExpr3 ],
   [ XPathStep, [ TOK_AT, XPathNodeTest ], 33,
     makeStepExpr4 ],
   [ XPathStep, [ XPathNodeTest ], 33,
     makeStepExpr5 ],
   [ XPathStep, [ XPathStep, XPathPredicate ], 33,
     makeStepExpr6 ],

   [ XPathNodeTest, [ TOK_ASTERISK ], 33,
     makeNodeTestExpr1 ],
   [ XPathNodeTest, [ TOK_NCNAME, TOK_COLON, TOK_ASTERISK ], 33,
     makeNodeTestExpr2 ],
   [ XPathNodeTest, [ TOK_QNAME ], 33,
     makeNodeTestExpr3 ],
   [ XPathNodeTest, [ TOK_NODEO, TOK_PARENC ], 33,
     makeNodeTestExpr4 ],
   [ XPathNodeTest, [ TOK_NODEO, XPathLiteral, TOK_PARENC ], 33,
     makeNodeTestExpr5 ],

   [ XPathPredicate, [ TOK_BRACKO, XPathExpr, TOK_BRACKC ], 33,
     makePredicateExpr ],

   [ XPathPrimaryExpr, [ XPathVariableReference ], 33,
     passExpr ],
   [ XPathPrimaryExpr, [ TOK_PARENO, XPathExpr, TOK_PARENC ], 33,
     makePrimaryExpr ],
   [ XPathPrimaryExpr, [ XPathLiteral ], 30,
     passExpr ],
   [ XPathPrimaryExpr, [ XPathNumber ], 30,
     passExpr ],
   [ XPathPrimaryExpr, [ XPathFunctionCall ], 30,
     passExpr ],

   [ XPathFunctionCall, [ TOK_QNAME, TOK_PARENO, TOK_PARENC ], -1,
     makeFunctionCallExpr1 ],
   [ XPathFunctionCall,
     [ TOK_QNAME, TOK_PARENO, XPathExpr, XPathArgumentRemainder, Q_MM,
       TOK_PARENC ], -1,
     makeFunctionCallExpr2 ],
   [ XPathArgumentRemainder, [ TOK_COMMA, XPathExpr ], -1,
     makeArgumentExpr ],

   [ XPathUnionExpr, [ XPathPathExpr ], 20,
     passExpr ],
   [ XPathUnionExpr, [ XPathUnionExpr, TOK_PIPE, XPathPathExpr ], 20,
     makeUnionExpr ],

   [ XPathPathExpr, [ XPathLocationPath ], 20,
     passExpr ],
   [ XPathPathExpr, [ XPathFilterExpr ], 19,
     passExpr ],
   [ XPathPathExpr,
     [ XPathFilterExpr, TOK_SLASH, XPathRelativeLocationPath ], 20,
     makePathExpr1 ],
   [ XPathPathExpr,
     [ XPathFilterExpr, TOK_DSLASH, XPathRelativeLocationPath ], 20,
     makePathExpr2 ],

   [ XPathFilterExpr, [ XPathPrimaryExpr, XPathPredicate, Q_MM ], 20,
     makeFilterExpr ],

   [ XPathExpr, [ XPathPrimaryExpr ], 16,
     passExpr ],
   [ XPathExpr, [ XPathUnionExpr ], 16,
     passExpr ],

   [ XPathExpr, [ TOK_MINUS, XPathExpr ], -1,
     makeUnaryMinusExpr ],

   [ XPathExpr, [ XPathExpr, TOK_OR, XPathExpr ], -1,
     makeBinaryExpr ],
   [ XPathExpr, [ XPathExpr, TOK_AND, XPathExpr ], -1,
     makeBinaryExpr ],

   [ XPathExpr, [ XPathExpr, TOK_EQ, XPathExpr ], -1,
     makeBinaryExpr ],
   [ XPathExpr, [ XPathExpr, TOK_NEQ, XPathExpr ], -1,
     makeBinaryExpr ],

   [ XPathExpr, [ XPathExpr, TOK_LT, XPathExpr ], -1,
     makeBinaryExpr ],
   [ XPathExpr, [ XPathExpr, TOK_LE, XPathExpr ], -1,
     makeBinaryExpr ],
   [ XPathExpr, [ XPathExpr, TOK_GT, XPathExpr ], -1,
     makeBinaryExpr ],
   [ XPathExpr, [ XPathExpr, TOK_GE, XPathExpr ], -1,
     makeBinaryExpr ],

   [ XPathExpr, [ XPathExpr, TOK_PLUS, XPathExpr ], -1,
     makeBinaryExpr, ASSOC_LEFT ],
   [ XPathExpr, [ XPathExpr, TOK_MINUS, XPathExpr ], -1,
     makeBinaryExpr, ASSOC_LEFT ],

   [ XPathExpr, [ XPathExpr, TOK_ASTERISK, XPathExpr ], -1,
     makeBinaryExpr, ASSOC_LEFT ],
   [ XPathExpr, [ XPathExpr, TOK_DIV, XPathExpr ], -1,
     makeBinaryExpr, ASSOC_LEFT ],
   [ XPathExpr, [ XPathExpr, TOK_MOD, XPathExpr ], -1,
     makeBinaryExpr, ASSOC_LEFT ],

   [ XPathLiteral, [ TOK_LITERALQ ], -1,
     makeLiteralExpr ],
   [ XPathLiteral, [ TOK_LITERALQQ ], -1,
     makeLiteralExpr ],

   [ XPathNumber, [ TOK_NUMBER ], -1,
     makeNumberExpr ],

   [ XPathVariableReference, [ TOK_DOLLAR, TOK_QNAME ], 200,
     makeVariableReference ]
   ];

// That function computes some optimizations of the above data
// structures and will be called right here. It merely takes the
// counter variables out of the global scope.

var xpathRules = [];

function xpathParseInit() {
  if (xpathRules.length) {
    return;
  }

  // Some simple optimizations for the xpath expression parser: sort
  // grammar rules descending by length, so that the longest match is
  // first found.

  xpathGrammarRules.sort(function(a,b) {
    var la = a[1].length;
    var lb = b[1].length;
    if (la < lb) {
      return 1;
    } else if (la > lb) {
      return -1;
    } else {
      return 0;
    }
  });

  var k = 1;
  for (var i = 0; i < xpathNonTerminals.length; ++i) {
    xpathNonTerminals[i].key = k++;
  }

  for (i = 0; i < xpathTokenRules.length; ++i) {
    xpathTokenRules[i].key = k++;
  }

  xpathLog('XPath parse INIT: ' + k + ' rules');

  // Another slight optimization: sort the rules into bins according
  // to the last element (observing quantifiers), so we can restrict
  // the match against the stack to the subest of rules that match the
  // top of the stack.
  //
  // TODO(mesch): What we actually want is to compute states as in
  // bison, so that we don't have to do any explicit and iterated
  // match against the stack.

  function push_(array, position, element) {
    if (!array[position]) {
      array[position] = [];
    }
    array[position].push(element);
  }

  for (i = 0; i < xpathGrammarRules.length; ++i) {
    var rule = xpathGrammarRules[i];
    var pattern = rule[1];

    for (var j = pattern.length - 1; j >= 0; --j) {
      if (pattern[j] == Q_1M) {
        push_(xpathRules, pattern[j-1].key, rule);
        break;

      } else if (pattern[j] == Q_MM || pattern[j] == Q_01) {
        push_(xpathRules, pattern[j-1].key, rule);
        --j;

      } else {
        push_(xpathRules, pattern[j].key, rule);
        break;
      }
    }
  }

  xpathLog('XPath parse INIT: ' + xpathRules.length + ' rule bins');

  var sum = 0;
  mapExec(xpathRules, function(i) {
    if (i) {
      sum += i.length;
    }
  });

  xpathLog('XPath parse INIT: ' + (sum / xpathRules.length) +
           ' average bin size');
}

// Local utility functions that are used by the lexer or parser.

function xpathCollectDescendants(nodelist, node) {
  for (var n = node.firstChild; n; n = n.nextSibling) {
    nodelist.push(n);
    arguments.callee(nodelist, n);
  }
}

function xpathCollectDescendantsReverse(nodelist, node) {
  for (var n = node.lastChild; n; n = n.previousSibling) {
    nodelist.push(n);
    arguments.callee(nodelist, n);
  }
}


// The entry point for the library: match an expression against a DOM
// node. Returns an XPath value.
function xpathDomEval(expr, node) {
  var expr1 = xpathParse(expr);
  var ret = expr1.evaluate(new ExprContext(node));
  return ret;
}

// Utility function to sort a list of nodes. Used by xsltSort() and
// nxslSelect().
function xpathSort(input, sort) {
  if (sort.length == 0) {
    return;
  }

  var sortlist = [];

  for (var i = 0; i < input.contextSize(); ++i) {
    var node = input.nodelist[i];
    var sortitem = { node: node, key: [] };
    var context = input.clone(node, 0, [ node ]);

    for (var j = 0; j < sort.length; ++j) {
      var s = sort[j];
      var value = s.expr.evaluate(context);

      var evalue;
      if (s.type == 'text') {
        evalue = value.stringValue();
      } else if (s.type == 'number') {
        evalue = value.numberValue();
      }
      sortitem.key.push({ value: evalue, order: s.order });
    }

    // Make the sort stable by adding a lowest priority sort by
    // id. This is very convenient and furthermore required by the
    // spec ([XSLT] - Section 10 Sorting).
    sortitem.key.push({ value: i, order: 'ascending' });

    sortlist.push(sortitem);
  }

  sortlist.sort(xpathSortByKey);

  var nodes = [];
  for (var i = 0; i < sortlist.length; ++i) {
    nodes.push(sortlist[i].node);
  }
  input.nodelist = nodes;
  input.setNode(0);
}


// Sorts by all order criteria defined. According to the JavaScript
// spec ([ECMA] Section 11.8.5), the compare operators compare strings
// as strings and numbers as numbers.
//
// NOTE: In browsers which do not follow the spec, this breaks only in
// the case that numbers should be sorted as strings, which is very
// uncommon.
function xpathSortByKey(v1, v2) {
  // NOTE: Sort key vectors of different length never occur in
  // xsltSort.

  for (var i = 0; i < v1.key.length; ++i) {
    var o = v1.key[i].order == 'descending' ? -1 : 1;
    if (v1.key[i].value > v2.key[i].value) {
      return +1 * o;
    } else if (v1.key[i].value < v2.key[i].value) {
      return -1 * o;
    }
  }

  return 0;
}


// Parses and then evaluates the given XPath expression in the given
// input context. Notice that parsed xpath expressions are cached.
function xpathEval(select, context) {
  var expr = xpathParse(select);
  var ret = expr.evaluate(context);
  return ret;
}

// SpryData.js - version 0.45 - Spry Pre-Release 1.6.1
//
// Copyright (c) 2006. Adobe Systems Incorporated.
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
//   * Redistributions of source code must retain the above copyright notice,
//     this list of conditions and the following disclaimer.
//   * Redistributions in binary form must reproduce the above copyright notice,
//     this list of conditions and the following disclaimer in the documentation
//     and/or other materials provided with the distribution.
//   * Neither the name of Adobe Systems Incorporated nor the names of its
//     contributors may be used to endorse or promote products derived from this
//     software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.

var Spry; if (!Spry) Spry = {};

//////////////////////////////////////////////////////////////////////
//
// Spry.Utils
//
//////////////////////////////////////////////////////////////////////

if (!Spry.Utils) Spry.Utils = {};

Spry.Utils.msProgIDs = ["MSXML2.XMLHTTP.6.0", "MSXML2.XMLHTTP.3.0"];

Spry.Utils.createXMLHttpRequest = function()
{
	var req = null;
	try
	{
		// Try to use the ActiveX version of XMLHttpRequest. This will
		// allow developers to load file URLs in IE7 when running in the
		// local zone.

		if (window.ActiveXObject)
		{
			while (!req && Spry.Utils.msProgIDs.length)
			{
				try { req = new ActiveXObject(Spry.Utils.msProgIDs[0]); } catch (e) { req = null; }
				if (!req)
					Spry.Utils.msProgIDs.splice(0, 1);
			}
		}

		// We're either running in a non-IE browser, or we failed to
		// create the ActiveX version of the XMLHttpRequest object.
		// Try to use the native version of XMLHttpRequest if it exists.

		if (!req && window.XMLHttpRequest)
			req = new XMLHttpRequest();
	}
	catch (e) { req = null;	}

	if (!req)
		Spry.Debug.reportError("Failed to create an XMLHttpRequest object!" );

	return req;
};

Spry.Utils.loadURL = function(method, url, async, callback, opts)
{
	var req = new Spry.Utils.loadURL.Request();
	req.method = method;
	req.url = url;
	req.async = async;
	req.successCallback = callback;
	Spry.Utils.setOptions(req, opts);

	try
	{
		req.xhRequest = Spry.Utils.createXMLHttpRequest();
		if (!req.xhRequest)
			return null;

		if (req.async)
			req.xhRequest.onreadystatechange = function() { Spry.Utils.loadURL.callback(req); };

		req.xhRequest.open(req.method, req.url, req.async, req.username, req.password);

		if (req.headers)
		{
			for (var name in req.headers)
				req.xhRequest.setRequestHeader(name, req.headers[name]);
		}

		req.xhRequest.send(req.postData);

		if (!req.async)
			Spry.Utils.loadURL.callback(req);
	}
	catch(e)
	{
		if (req.errorCallback)
			req.errorCallback(req);
		else
			Spry.Debug.reportError("Exception caught while loading " + url + ": " + e);
		req = null;
	}

	return req;
};

Spry.Utils.loadURL.callback = function(req)
{
	if (!req || req.xhRequest.readyState != 4)
		return;
	if (req.successCallback && (req.xhRequest.status == 200 || req.xhRequest.status == 0))
		req.successCallback(req);
	else if (req.errorCallback)
		req.errorCallback(req);
};

Spry.Utils.loadURL.Request = function()
{
	var props = Spry.Utils.loadURL.Request.props;
	var numProps = props.length;

	for (var i = 0; i < numProps; i++)
		this[props[i]] = null;

	this.method = "GET";
	this.async = true;
	this.headers = {};
};

Spry.Utils.loadURL.Request.props = [ "method", "url", "async", "username", "password", "postData", "successCallback", "errorCallback", "headers", "userData", "xhRequest" ];

Spry.Utils.loadURL.Request.prototype.extractRequestOptions = function(opts, undefineRequestProps)
{
	if (!opts)
		return;

	var props = Spry.Utils.loadURL.Request.props;
	var numProps = props.length;

	for (var i = 0; i < numProps; i++)
	{
		var prop = props[i];
		if (opts[prop] != undefined)
		{
			this[prop] = opts[prop];
			if (undefineRequestProps)
				opts[prop] = undefined;
		}
	}
};

Spry.Utils.loadURL.Request.prototype.clone = function()
{
	var props = Spry.Utils.loadURL.Request.props;
	var numProps = props.length;
	var req = new Spry.Utils.loadURL.Request;
	for (var i = 0; i < numProps; i++)
		req[props[i]] = this[props[i]];
	if (this.headers)
	{
		req.headers = {};
		Spry.Utils.setOptions(req.headers, this.headers);
	}
	return req;
};

Spry.Utils.setInnerHTML = function(ele, str, preventScripts)
{
	if (!ele)
		return;
	ele = Spry.$(ele);
	var scriptExpr = "<script[^>]*>(.|\s|\n|\r)*?</script>";
	ele.innerHTML = str.replace(new RegExp(scriptExpr, "img"), "");

	if (preventScripts)
		return;

	var matches = str.match(new RegExp(scriptExpr, "img"));
	if (matches)
	{
		var numMatches = matches.length;
		for (var i = 0; i < numMatches; i++)
		{
			var s = matches[i].replace(/<script[^>]*>[\s\r\n]*(<\!--)?|(-->)?[\s\r\n]*<\/script>/img, "");
			Spry.Utils.eval(s);
		}
	}
};

Spry.Utils.updateContent = function (ele, url, finishFunc, opts)
{
	Spry.Utils.loadURL("GET", url, true, function(req)
	{
		Spry.Utils.setInnerHTML(ele, req.xhRequest.responseText);
		if (finishFunc)
			finishFunc(ele, url);
	}, opts);
};

//////////////////////////////////////////////////////////////////////
//
// Functions from SpryDOMUtils.js
//   - These have been left in for backwards compatibility, but they
//     should only be defined if Spry.$$ (SpryDOMUtils.js) is not
//     already included.
//   - If SpryDOMUtils.js is included *after* SpryData.js, these
//     functions will be replaced with the latest versions in
//     SpryDOMUtils.js.
//
//////////////////////////////////////////////////////////////////////

if (!Spry.$$)
{
Spry.Utils.addEventListener = function(element, eventType, handler, capture)
{
	try
	{
		element = Spry.$(element);
		if (element.addEventListener)
			element.addEventListener(eventType, handler, capture);
		else if (element.attachEvent)
			element.attachEvent("on" + eventType, handler);
	}
	catch (e) {}
};

Spry.Utils.removeEventListener = function(element, eventType, handler, capture)
{
	try
	{
		element = Spry.$(element);
		if (element.removeEventListener)
			element.removeEventListener(eventType, handler, capture);
		else if (element.detachEvent)
			element.detachEvent("on" + eventType, handler);
	}
	catch (e) {}
};

Spry.Utils.addLoadListener = function(handler)
{
	if (typeof window.addEventListener != 'undefined')
		window.addEventListener('load', handler, false);
	else if (typeof document.addEventListener != 'undefined')
		document.addEventListener('load', handler, false);
	else if (typeof window.attachEvent != 'undefined')
		window.attachEvent('onload', handler);
};

Spry.Utils.addClassName = function(ele, className)
{
	ele = Spry.$(ele);
	if (!ele || !className || (ele.className && ele.className.search(new RegExp("\\b" + className + "\\b")) != -1))
		return;
	ele.className += (ele.className ? " " : "") + className;
};

Spry.Utils.removeClassName = function(ele, className)
{
	ele = Spry.$(ele);
	if (!ele || !className || (ele.className && ele.className.search(new RegExp("\\b" + className + "\\b")) == -1))
		return;
	ele.className = ele.className.replace(new RegExp("\\s*\\b" + className + "\\b", "g"), "");
};

Spry.Utils.getObjectByName = function(name)
{
	var result = null;
	if (name)
	{
		var lu = window;
		var objPath = name.split(".");
		for (var i = 0; lu && i < objPath.length; i++)
		{
			result = lu[objPath[i]];
			lu = result;
		}
	}
	return result;
};

//////////////////////////////////////////////////////////////////////
//
// Define Prototype's $() convenience function, but make sure it is
// namespaced under Spry so that we avoid collisions with other
// toolkits.
//
//////////////////////////////////////////////////////////////////////

Spry.$ = function(element)
{
	if (arguments.length > 1)
	{
		for (var i = 0, elements = [], length = arguments.length; i < length; i++)
			elements.push(Spry.$(arguments[i]));
		return elements;
	}
	if (typeof element == 'string')
		element = document.getElementById(element);
	return element;
};
} // if (!Spry.$$)

//////////////////////////////////////////////////////////////////////

Spry.Utils.eval = function(str)
{
	// Call this method from your JS function when
	// you don't want the JS expression to access or
	// interfere with any local variables in your JS
	// function.

	return eval(str);
};

Spry.Utils.escapeQuotesAndLineBreaks = function(str)
{
	if (str)
	{
		str = str.replace(/\\/g, "\\\\");
		str = str.replace(/["']/g, "\\$&");
		str = str.replace(/\n/g, "\\n");
		str = str.replace(/\r/g, "\\r");
	}
	return str;
};

Spry.Utils.encodeEntities = function(str)
{
	if (str && str.search(/[&<>"]/) != -1)
	{
		str = str.replace(/&/g, "&amp;");
		str = str.replace(/</g, "&lt;");
		str = str.replace(/>/g, "&gt;");
		str = str.replace(/"/g, "&quot;");
	}
	return str
};

Spry.Utils.decodeEntities = function(str)
{
	var d = Spry.Utils.decodeEntities.div;
	if (!d)
	{
		d = document.createElement('div');
		Spry.Utils.decodeEntities.div = d;
		if (!d) return str;
	}
	d.innerHTML = str;
	if (d.childNodes.length == 1 && d.firstChild.nodeType == 3 /* Node.TEXT_NODE */ && d.firstChild.nextSibling == null)
		str = d.firstChild.data;
	else
	{
		// Hmmm, innerHTML processing of str produced content
		// we weren't expecting, so just replace entities we
		// expect folks will use in node attributes that contain
		// JavaScript.
		str = str.replace(/&lt;/gi, "<");
		str = str.replace(/&gt;/gi, ">");
		str = str.replace(/&quot;/gi, "\"");
		str = str.replace(/&amp;/gi, "&");
	}
	return str;
};

Spry.Utils.fixupIETagAttributes = function(inStr)
{
	var outStr = "";

	// Break the tag string into 3 pieces.

	var tagStart = inStr.match(/^<[^\s>]+\s*/)[0];
	var tagEnd = inStr.match(/\s*\/?>$/)[0];
	var tagAttrs = inStr.replace(/^<[^\s>]+\s*|\s*\/?>/g, "");

	// Write out the start of the tag.
	outStr += tagStart;

	// If the tag has attributes, parse it out manually to avoid accidentally fixing up
	// attributes that contain JavaScript expressions.

	if (tagAttrs)
	{
		var startIndex = 0;
		var endIndex = 0;

		while (startIndex < tagAttrs.length)
		{
			// Find the '=' char of the attribute.
			while (tagAttrs.charAt(endIndex) != '=' && endIndex < tagAttrs.length)
				++endIndex;

			// If we are at the end of the string, just write out what we've
			// collected.

			if (endIndex >= tagAttrs.length)
			{
				outStr += tagAttrs.substring(startIndex, endIndex);
				break;
			}

			// Step past the '=' character and write out what we've
			// collected so far.

			++endIndex;
			outStr += tagAttrs.substring(startIndex, endIndex);
			startIndex = endIndex;

			if (tagAttrs.charAt(endIndex) == '"' || tagAttrs.charAt(endIndex) == "'")
			{
				// Attribute is quoted. Advance us past the quoted value!
				var savedIndex = endIndex++;
				while (endIndex < tagAttrs.length)
				{
					if (tagAttrs.charAt(endIndex) == tagAttrs.charAt(savedIndex))
					{
						endIndex++;
						break;
					}
					else if (tagAttrs.charAt(endIndex) == "\\")

						endIndex++;
					endIndex++;
				}

				outStr += tagAttrs.substring(startIndex, endIndex);
				startIndex = endIndex;
			}
			else
			{
				// This attribute value wasn't quoted! Wrap it with quotes and
				// write out everything till we hit a space, or the end of the
				// string.

				outStr += "\"";

				var sIndex = tagAttrs.slice(endIndex).search(/\s/);
				endIndex = (sIndex != -1) ? (endIndex + sIndex) : tagAttrs.length;
				outStr += tagAttrs.slice(startIndex, endIndex);
				outStr += "\"";
				startIndex = endIndex;
			}
		}
	}

	outStr += tagEnd;

	// Write out the end of the tag.
	return outStr;
};

Spry.Utils.fixUpIEInnerHTML = function(inStr)
{
	var outStr = "";

	// Create a regular expression that will match:
	//     <!--
	//     <![CDATA[
	//     <tag>
	//     -->
	//     ]]>
	//     ]]&gt;   // Yet another workaround for an IE innerHTML bug.
	//
	// The idea here is that we only want to fix up attribute values on tags that
	// are not in any comments or CDATA.

	var regexp = new RegExp("<\\!--|<\\!\\[CDATA\\[|<\\w+[^<>]*>|-->|\\]\\](>|\&gt;)", "g");
	var searchStartIndex = 0;
	var skipFixUp = 0;

	while (inStr.length)
	{
		var results = regexp.exec(inStr);
		if (!results || !results[0])
		{
			outStr += inStr.substr(searchStartIndex, inStr.length - searchStartIndex);
			break;
		}

		if (results.index != searchStartIndex)
		{
			// We found a match but it's not at the start of the inStr.
			// Create a string token for everything that precedes the match.
			outStr += inStr.substr(searchStartIndex, results.index - searchStartIndex);
		}

		if (results[0] == "<!--" || results[0] == "<![CDATA[")
		{
			++skipFixUp;
			outStr += results[0];
		}
		else if (results[0] == "-->" || results[0] == "]]>" || (skipFixUp && results[0] == "]]&gt;"))
		{
			--skipFixUp;
			outStr += results[0];
		}
		else if (!skipFixUp && results[0].charAt(0) == '<')
			outStr += Spry.Utils.fixupIETagAttributes(results[0]);
		else
			outStr += results[0];

		searchStartIndex = regexp.lastIndex;
	}

	return outStr;
};

Spry.Utils.stringToXMLDoc = function(str)
{
	var xmlDoc = null;

	try
	{
		// Attempt to parse the string using the IE method.

		var xmlDOMObj = new ActiveXObject("Microsoft.XMLDOM");
		xmlDOMObj.async = false;
		xmlDOMObj.loadXML(str);
		xmlDoc = xmlDOMObj;
	}
	catch (e)
	{
		// The IE method didn't work. Try the Mozilla way.

		try
		{
			var domParser = new DOMParser;
			xmlDoc = domParser.parseFromString(str, 'text/xml');
		}
		catch (e)
		{
			Spry.Debug.reportError("Caught exception in Spry.Utils.stringToXMLDoc(): " + e + "\n");
			xmlDoc = null;
		}
	}

	return xmlDoc;
};

Spry.Utils.serializeObject = function(obj)
{
	// Create a JSON representation of a given object.

	var str = "";
	var firstItem = true;

	if (obj == null || obj == undefined)
		return str + obj;

	var objType = typeof obj;

	if (objType == "number" || objType == "boolean")
		str += obj;
	else if (objType == "string")
		str += "\"" + Spry.Utils.escapeQuotesAndLineBreaks(obj) + "\"";
	else if (obj.constructor == Array)
	{
		str += "[";
		for (var i = 0; i < obj.length; i++)
		{
			if (!firstItem)
				str += ", ";
			str += Spry.Utils.serializeObject(obj[i]);
			firstItem = false;
		}
		str += "]";
	}
	else if (objType == "object")
	{
		str += "{";
		for (var p in obj)
		{
			if (!firstItem)
				str += ", ";
			str += "\"" + p + "\": " + Spry.Utils.serializeObject(obj[p]);
			firstItem = false;
		}
		str += "}";
	}
	return str;
};

Spry.Utils.getNodesByFunc = function(root, func)
{
	var nodeStack = new Array;
	var resultArr = new Array;
	var node = root;

	while (node)
	{
		if (func(node))
			resultArr.push(node);

		if (node.hasChildNodes())
		{
			nodeStack.push(node);
			node = node.firstChild;
		}
		else
		{
			if (node == root)
				node = null;
			else
				try { node = node.nextSibling; } catch (e) { node = null; };
		}

		while (!node && nodeStack.length > 0)
		{
			node = nodeStack.pop();
			if (node == root)
				node = null;
			else
				try { node = node.nextSibling; } catch (e) { node = null; }
		}
	}

	if (nodeStack && nodeStack.length > 0)
		Spry.Debug.trace("-- WARNING: Spry.Utils.getNodesByFunc() failed to traverse all nodes!\n");

	return resultArr;
};

// XXX: UNUSED FUNCTION
Spry.Utils.getFirstChildWithNodeName = function(node, nodeName)
{
	var child = node.firstChild;

	while (child)
	{
		if (child.nodeName == nodeName)
			return child;
		child = child.nextSibling;
	}

	return null;
};

Spry.Utils.setOptions = function(obj, optionsObj, ignoreUndefinedProps)
{
	if (!optionsObj)
		return;

	for (var optionName in optionsObj)
	{
		if (ignoreUndefinedProps && optionsObj[optionName] == undefined)
			continue;
		obj[optionName] = optionsObj[optionName];
	}
};

Spry.Utils.SelectionManager = {};
Spry.Utils.SelectionManager.selectionGroups = new Object;

Spry.Utils.SelectionManager.SelectionGroup = function()
{
	this.selectedElements = new Array;
};

Spry.Utils.SelectionManager.SelectionGroup.prototype.select = function(element, className, multiSelect)
{
	var selObj = null;

	if (!multiSelect)
	{
		// Multiple selection is not enabled, so clear any
		// selected elements from our list.

		this.clearSelection();
	}
	else
	{
		// Multiple selection is enabled, so check to see if element
		// is already in the array. If it is, make sure the className
		// is the className that was passed in.

		for (var i = 0; i < this.selectedElements.length; i++)
		{
			selObj = this.selectedElements[i].element;

			if (selObj.element == element)
			{
				if (selObj.className != className)
				{
					Spry.Utils.removeClassName(element, selObj.className);
					Spry.Utils.addClassName(element, className);
				}
				return;
			}
		}
	}

	// Add the element to our list of selected elements.

	selObj = new Object;
	selObj.element = element;
	selObj.className = className;
	this.selectedElements.push(selObj);
	Spry.Utils.addClassName(element, className);
};

Spry.Utils.SelectionManager.SelectionGroup.prototype.unSelect = function(element)
{
	for (var i = 0; i < this.selectedElements.length; i++)
	{
		var selObj = this.selectedElements[i].element;

		if (selObj.element == element)
		{
			Spry.Utils.removeClassName(selObj.element, selObj.className);
			return;
		}
	}
};

Spry.Utils.SelectionManager.SelectionGroup.prototype.clearSelection = function()
{
	var selObj = null;

	do
	{
		selObj = this.selectedElements.shift();
		if (selObj)
			Spry.Utils.removeClassName(selObj.element, selObj.className);
	}
	while (selObj);
};

Spry.Utils.SelectionManager.getSelectionGroup = function(selectionGroupName)
{
	if (!selectionGroupName)
		return null;

	var groupObj = Spry.Utils.SelectionManager.selectionGroups[selectionGroupName];

	if (!groupObj)
	{
		groupObj = new Spry.Utils.SelectionManager.SelectionGroup();
		Spry.Utils.SelectionManager.selectionGroups[selectionGroupName] = groupObj;
	}

	return groupObj;
};

Spry.Utils.SelectionManager.select = function(selectionGroupName, element, className, multiSelect)
{
	var groupObj = Spry.Utils.SelectionManager.getSelectionGroup(selectionGroupName);

	if (!groupObj)
		return;

	groupObj.select(element, className, multiSelect);
};

Spry.Utils.SelectionManager.unSelect = function(selectionGroupName, element)
{
	var groupObj = Spry.Utils.SelectionManager.getSelectionGroup(selectionGroupName);

	if (!groupObj)
		return;

	groupObj.unSelect(element, className);
};

Spry.Utils.SelectionManager.clearSelection = function(selectionGroupName)
{
	var groupObj = Spry.Utils.SelectionManager.getSelectionGroup(selectionGroupName);

	if (!groupObj)
		return;

	groupObj.clearSelection();
};

Spry.Utils.Notifier = function()
{
	this.observers = [];
	this.suppressNotifications = 0;
};

Spry.Utils.Notifier.prototype.addObserver = function(observer)
{
	if (!observer)
		return;

	// Make sure the observer isn't already on the list.

	var len = this.observers.length;
	for (var i = 0; i < len; i++)
	{
		if (this.observers[i] == observer)
			return;
	}
	this.observers[len] = observer;
};

Spry.Utils.Notifier.prototype.removeObserver = function(observer)
{
	if (!observer)
		return;

	for (var i = 0; i < this.observers.length; i++)
	{
		if (this.observers[i] == observer)
		{
			this.observers.splice(i, 1);
			break;
		}
	}
};

Spry.Utils.Notifier.prototype.notifyObservers = function(methodName, data)
{
	if (!methodName)
		return;

	if (!this.suppressNotifications)
	{
		var len = this.observers.length;
		for (var i = 0; i < len; i++)
		{
			var obs = this.observers[i];
			if (obs)
			{
				if (typeof obs == "function")
					obs(methodName, this, data);
				else if (obs[methodName])
					obs[methodName](this, data);
			}
		}
	}
};

Spry.Utils.Notifier.prototype.enableNotifications = function()
{
	if (--this.suppressNotifications < 0)
	{
		this.suppressNotifications = 0;
		Spry.Debug.reportError("Unbalanced enableNotifications() call!\n");
	}
};

Spry.Utils.Notifier.prototype.disableNotifications = function()
{
	++this.suppressNotifications;
};

//////////////////////////////////////////////////////////////////////
//
// Spry.Debug
//
//////////////////////////////////////////////////////////////////////

Spry.Debug = {};
Spry.Debug.enableTrace = true;
Spry.Debug.debugWindow = null;
Spry.Debug.onloadDidFire = false;

Spry.Utils.addLoadListener(function() { Spry.Debug.onloadDidFire = true; Spry.Debug.flushQueuedMessages(); });

Spry.Debug.flushQueuedMessages = function()
{
	if (Spry.Debug.flushQueuedMessages.msgs)
	{
		var msgs = Spry.Debug.flushQueuedMessages.msgs;
		for (var i = 0; i < msgs.length; i++)
			Spry.Debug.debugOut(msgs[i].msg, msgs[i].color);
		Spry.Debug.flushQueuedMessages.msgs = null;
	}
};

Spry.Debug.createDebugWindow = function()
{
	if (!Spry.Debug.enableTrace || Spry.Debug.debugWindow || !Spry.Debug.onloadDidFire)
		return;
	try
	{
		Spry.Debug.debugWindow = document.createElement("div");
		var div = Spry.Debug.debugWindow;
		div.style.fontSize = "12px";
		div.style.fontFamily = "console";
		div.style.position = "absolute";
		div.style.width = "400px";
		div.style.height = "300px";
		div.style.overflow = "auto";
		div.style.border = "solid 1px black";
		div.style.backgroundColor = "white";
		div.style.color = "black";
		div.style.bottom = "0px";
		div.style.right = "0px";
		// div.style.opacity = "0.5";
		// div.style.filter = "alpha(opacity=50)";
		div.setAttribute("id", "SpryDebugWindow");
		document.body.appendChild(Spry.Debug.debugWindow);
	}
	catch (e) {}
};

Spry.Debug.debugOut = function(str, bgColor)
{
	if (!Spry.Debug.debugWindow)
	{
		Spry.Debug.createDebugWindow();
		if (!Spry.Debug.debugWindow)
		{
			if (!Spry.Debug.flushQueuedMessages.msgs)
				Spry.Debug.flushQueuedMessages.msgs = new Array;
			Spry.Debug.flushQueuedMessages.msgs.push({msg: str, color: bgColor});
			return;
		}
	}

	var d = document.createElement("div");
	if (bgColor)
		d.style.backgroundColor = bgColor;
	d.innerHTML = str;
	Spry.Debug.debugWindow.appendChild(d);
};

Spry.Debug.trace = function(str)
{
	Spry.Debug.debugOut(str);
};

Spry.Debug.reportError = function(str)
{
	Spry.Debug.debugOut(str, "red");
};

//////////////////////////////////////////////////////////////////////
//
// Spry.Data
//
//////////////////////////////////////////////////////////////////////

Spry.Data = {};
Spry.Data.regionsArray = {};
Spry.Data.initRegionsOnLoad = true;

Spry.Data.initRegions = function(rootNode)
{
	rootNode = rootNode ? Spry.$(rootNode) : document.body;

	var lastRegionFound = null;

	var regions = Spry.Utils.getNodesByFunc(rootNode, function(node)
	{
		try
		{
			if (node.nodeType != 1 /* Node.ELEMENT_NODE */)
				return false;

			// Region elements must have an spryregion attribute with a
			// non-empty value. An id attribute is also required so we can
			// reference the region by name if necessary.

			var attrName = "spry:region";
			var attr = node.attributes.getNamedItem(attrName);
			if (!attr)
			{
				attrName = "spry:detailregion";
				attr = node.attributes.getNamedItem(attrName);
			}
			if (attr)
			{
				if (lastRegionFound)
				{
					var parent = node.parentNode;
					while (parent)
					{
						if (parent == lastRegionFound)
						{
							Spry.Debug.reportError("Found a nested " + attrName + " in the following markup. Nested regions are currently not supported.<br/><pre>" + Spry.Utils.encodeEntities(parent.innerHTML) + "</pre>");
							return false;
						}
						parent = parent.parentNode;
					}
				}

				if (attr.value)
				{
					attr = node.attributes.getNamedItem("id");
					if (!attr || !attr.value)
					{
						// The node is missing an id attribute so add one.
						node.setAttribute("id", "spryregion" + (++Spry.Data.initRegions.nextUniqueRegionID));
					}

					lastRegionFound = node;
					return true;
				}
				else
					Spry.Debug.reportError(attrName + " attributes require one or more data set names as values!");
			}
		}
		catch(e) {}
		return false;
	});

	var name, dataSets, i;
	var newRegions = [];

	for (i = 0; i < regions.length; i++)
	{
		var rgn = regions[i];

		var isDetailRegion = false;

		// Get the region name.
		name = rgn.attributes.getNamedItem("id").value;

		attr = rgn.attributes.getNamedItem("spry:region");
		if (!attr)
		{
			attr = rgn.attributes.getNamedItem("spry:detailregion");
			isDetailRegion = true;
		}

		if (!attr.value)
		{
			Spry.Debug.reportError("spry:region and spry:detailregion attributes require one or more data set names as values!");
			continue;
		}

		// Remove the spry:region or spry:detailregion attribute so it doesn't appear in
		// the output generated by our processing of the dynamic region.
		rgn.attributes.removeNamedItem(attr.nodeName);

		// Remove the hiddenRegionCSS class from the rgn.
		Spry.Utils.removeClassName(rgn, Spry.Data.Region.hiddenRegionClassName);

		// Get the DataSets that should be bound to the region.
		dataSets = Spry.Data.Region.strToDataSetsArray(attr.value);

		if (!dataSets.length)
		{
			Spry.Debug.reportError("spry:region or spry:detailregion attribute has no data set!");
			continue;
		}

		var hasBehaviorAttributes = false;
		var hasSpryContent = false;
		var dataStr = "";

		var parent = null;
		var regionStates = {};
		var regionStateMap = {};

		// Check if there are any attributes on the region node that remap
		// the default states.

		attr = rgn.attributes.getNamedItem("spry:readystate");
		if (attr && attr.value)
			regionStateMap["ready"] = attr.value;
		attr = rgn.attributes.getNamedItem("spry:errorstate");
		if (attr && attr.value)
			regionStateMap["error"] = attr.value;
		attr = rgn.attributes.getNamedItem("spry:loadingstate");
		if (attr && attr.value)
			regionStateMap["loading"] = attr.value;
		attr = rgn.attributes.getNamedItem("spry:expiredstate");
		if (attr && attr.value)
			regionStateMap["expired"] = attr.value;

		// Find all of the processing instruction regions in the region.
		// Insert comments around the regions we find so we can identify them
		// easily when tokenizing the region html string.

		var piRegions = Spry.Utils.getNodesByFunc(rgn, function(node)
		{
			try
			{
				if (node.nodeType == 1 /* ELEMENT_NODE */)
				{
					var attributes = node.attributes;
					var numPI = Spry.Data.Region.PI.orderedInstructions.length;
					var lastStartComment = null;
					var lastEndComment = null;

					for (var i = 0; i < numPI; i++)
					{
						var piName = Spry.Data.Region.PI.orderedInstructions[i];
						var attr = attributes.getNamedItem(piName);
						if (!attr)
							continue;

						var piDesc = Spry.Data.Region.PI.instructions[piName];
						var childrenOnly = (node == rgn) ? true : piDesc.childrenOnly;
						var openTag = piDesc.getOpenTag(node, piName);
						var closeTag = piDesc.getCloseTag(node, piName);

						if (childrenOnly)
						{
								var oComment = document.createComment(openTag);
								var cComment = document.createComment(closeTag);

								if (!lastStartComment)
									node.insertBefore(oComment, node.firstChild);
								else
									node.insertBefore(oComment, lastStartComment.nextSibling);
								lastStartComment = oComment;

								if (!lastEndComment)
									node.appendChild(cComment);
								else
									node.insertBefore(cComment, lastEndComment);
								lastEndComment = cComment;
						}
						else
						{
							var parent = node.parentNode;
							parent.insertBefore(document.createComment(openTag), node);
							parent.insertBefore(document.createComment(closeTag), node.nextSibling);
						}

						// If this is a spry:state processing instruction, record the state name
						// so we know that we should re-generate the region if we ever see that state.

						if (piName == "spry:state")
							regionStates[attr.value] = true;

						node.removeAttribute(piName);
					}

					if (Spry.Data.Region.enableBehaviorAttributes)
					{
						var bAttrs = Spry.Data.Region.behaviorAttrs;
						for (var behaviorAttrName in bAttrs)
						{
							var bAttr = attributes.getNamedItem(behaviorAttrName);
							if (bAttr)
							{
								hasBehaviorAttributes = true;
								if (bAttrs[behaviorAttrName].setup)
									bAttrs[behaviorAttrName].setup(node, bAttr.value);
							}
						}
					}
				}
			}
			catch(e) {}
			return false;
		});

		// Get the data in the region.
		dataStr = rgn.innerHTML;

		// Argh! IE has an innerHTML bug where it will remove the quotes around any
		// attribute value that it thinks is a single word. This includes removing quotes
		// around our data references which is problematic since a single data reference
		// can be replaced with multiple words. If we are running in IE, we have to call
		// fixUpIEInnerHTML to get around this problem.

		if (window.ActiveXObject && !Spry.Data.Region.disableIEInnerHTMLFixUp && dataStr.search(/=\{/) != -1)
		{
			if (Spry.Data.Region.debug)
				Spry.Debug.trace("<hr />Performing IE innerHTML fix up of Region: " + name + "<br /><br />" + Spry.Utils.encodeEntities(dataStr));

			dataStr = Spry.Utils.fixUpIEInnerHTML(dataStr);
		}

		if (Spry.Data.Region.debug)
			Spry.Debug.trace("<hr />Region template markup for '" + name + "':<br /><br />" + Spry.Utils.encodeEntities(dataStr));

		if (!hasSpryContent)
		{
			// Clear the region.
			rgn.innerHTML = "";
		}

		// Create a Spry.Data.Region object for this region.
		var region = new Spry.Data.Region(rgn, name, isDetailRegion, dataStr, dataSets, regionStates, regionStateMap, hasBehaviorAttributes);
		Spry.Data.regionsArray[region.name] = region;
		newRegions.push(region);
	}

	for (var i = 0; i < newRegions.length; i++)
		newRegions[i].updateContent();
};

Spry.Data.initRegions.nextUniqueRegionID = 0;

Spry.Data.updateRegion = function(regionName)
{
	if (!regionName || !Spry.Data.regionsArray || !Spry.Data.regionsArray[regionName])
		return;

	try { Spry.Data.regionsArray[regionName].updateContent(); }
	catch(e) { Spry.Debug.reportError("Spry.Data.updateRegion(" + regionName + ") caught an exception: " + e + "\n"); }
};

Spry.Data.getRegion = function(regionName)
{
	return Spry.Data.regionsArray[regionName];
};


Spry.Data.updateAllRegions = function()
{
	if (!Spry.Data.regionsArray)
		return;

	for (var regionName in Spry.Data.regionsArray)
		Spry.Data.updateRegion(regionName);
};

Spry.Data.getDataSetByName = function(dataSetName)
{
	// Currently, there is no registry of mappings between
	// data set names and data set objects. For now, the assumption
	// is that the user has declared and created a data set in the
	// global space.
	//
	// We check for the presence of a global variable with the
	// specified name, and then make sure that its value is an
	// object with at least 2 of the data set base functions defined.

	var ds = window[dataSetName];
	if (typeof ds != "object" || !ds.getData || !ds.filter)
		return null;
	return ds;
};

//////////////////////////////////////////////////////////////////////
//
// Spry.Data.DataSet
//
//////////////////////////////////////////////////////////////////////

Spry.Data.DataSet = function(options)
{
	Spry.Utils.Notifier.call(this);

	this.name = "";
	this.internalID = Spry.Data.DataSet.nextDataSetID++;
	this.curRowID = 0;
	this.data = [];
	this.unfilteredData = null;
	this.dataHash = {};
	this.columnTypes = {};
	this.filterFunc = null;		// non-destructive filter function
	this.filterDataFunc = null;	// destructive filter function

	this.distinctOnLoad = false;
	this.distinctFieldsOnLoad = null;
	this.sortOnLoad = null;
	this.sortOrderOnLoad = "ascending";
	this.keepSorted = false;

	this.dataWasLoaded = false;
	this.pendingRequest = null;

	this.lastSortColumns = [];
	this.lastSortOrder = "";

	this.loadIntervalID = 0;

	Spry.Utils.setOptions(this, options);
};

Spry.Data.DataSet.prototype = new Spry.Utils.Notifier();
Spry.Data.DataSet.prototype.constructor = Spry.Data.DataSet;

Spry.Data.DataSet.prototype.getData = function(unfiltered)
{
	return (unfiltered && this.unfilteredData) ? this.unfilteredData : this.data;
};

Spry.Data.DataSet.prototype.getUnfilteredData = function()
{
	// XXX: Deprecated.
	return this.getData(true);
};

Spry.Data.DataSet.prototype.getLoadDataRequestIsPending = function()
{
	return this.pendingRequest != null;
};

Spry.Data.DataSet.prototype.getDataWasLoaded = function()
{
	return this.dataWasLoaded;
};

Spry.Data.DataSet.prototype.getValue = function(valueName, rowContext)
{
	var result = undefined;

	// If a rowContext is not defined, we default to
	// using the current row.

	if (!rowContext)
		rowContext = this.getCurrentRow();

	switch(valueName)
	{
		case "ds_RowNumber":
			result = this.getRowNumber(rowContext);
			break;
		case "ds_RowNumberPlus1":
			result = this.getRowNumber(rowContext) + 1;
			break;
		case "ds_RowCount":
			result = this.getRowCount();
			break;
		case "ds_UnfilteredRowCount":
			result = this.getRowCount(true);
			break;
		case "ds_CurrentRowNumber":
			result = this.getCurrentRowNumber();
			break;
		case "ds_CurrentRowID":
			result = this.getCurrentRowID();
			break;
		case "ds_EvenOddRow":
			result = (this.getRowNumber(rowContext) % 2) ? Spry.Data.Region.evenRowClassName : Spry.Data.Region.oddRowClassName;
			break;
		case "ds_SortOrder":
			result = this.getSortOrder();
			break;
		case "ds_SortColumn":
			result = this.getSortColumn();
			break;
		default:
			// We have an unknown value, check to see if the current
			// row has column value that matches the valueName.
			if (rowContext)
				result = rowContext[valueName];
			break;
	}

	return result;
};

Spry.Data.DataSet.prototype.setDataFromArray = function(arr, fireSyncLoad)
{
	this.notifyObservers("onPreLoad");

	this.unfilteredData = null;
	this.filteredData = null;
	this.data = [];
	this.dataHash = {};

	var arrLen = arr.length;

	for (var i = 0; i < arrLen; i++)
	{
		var row = arr[i];
		if (row.ds_RowID == undefined)
			row.ds_RowID = i;
		this.dataHash[row.ds_RowID] = row;
		this.data.push(row);
	}

	this.loadData(fireSyncLoad);
};

Spry.Data.DataSet.prototype.loadData = function(syncLoad)
{
	// The idea here is that folks using the base class DataSet directly
	// would change the data in the DataSet manually and then call loadData()
	// to fire off an async notifications to say that it was ready for consumption.
	//
	// Firing off data changed notificataions synchronously from this method
	// can wreak havoc with complicated master/detail regions that use data sets
	// that have master/detail relationships with other data sets. Our data set
	// logic already handles async data loading nicely so we use a timer to fire
	// off the data changed notification to insure that it happens after this
	// function is finished and the JS stack unwinds.
	//
	// Other classes that derive from this class and load data synchronously
	// inside their loadData() implementation should also fire off an async
	// notification in this same manner to avoid this same problem.

	var self = this;

	this.pendingRequest = new Object;
	this.dataWasLoaded = false;

	var loadCallbackFunc = function()
	{
		self.pendingRequest = null;
		self.dataWasLoaded = true;

		self.applyColumnTypes();

		self.disableNotifications();
		self.filterAndSortData();
		self.enableNotifications();

		self.notifyObservers("onPostLoad");
		self.notifyObservers("onDataChanged");
	};

	if (syncLoad)
		loadCallbackFunc();
	else
		this.pendingRequest.timer = setTimeout(loadCallbackFunc, 0);
};


Spry.Data.DataSet.prototype.filterAndSortData = function()
{
	// If there is a data filter installed, run it.

	if (this.filterDataFunc)
		this.filterData(this.filterDataFunc, true);

	// If the distinct flag was set, run through all the records in the recordset
	// and toss out any that are duplicates.

	if (this.distinctOnLoad)
		this.distinct(this.distinctFieldsOnLoad);

	// If sortOnLoad was set, sort the data based on the columns
	// specified in sortOnLoad.

	if (this.keepSorted && this.getSortColumn())
		this.sort(this.lastSortColumns, this.lastSortOrder);
	else if (this.sortOnLoad)
		this.sort(this.sortOnLoad, this.sortOrderOnLoad);

	// If there is a view filter installed, run it.

	if (this.filterFunc)
		this.filter(this.filterFunc, true);

	// The default "current" row is the first row of the data set.
	if (this.data && this.data.length > 0)
		this.curRowID = this.data[0]['ds_RowID'];
	else
		this.curRowID = 0;
};

Spry.Data.DataSet.prototype.cancelLoadData = function()
{
	if (this.pendingRequest && this.pendingRequest.timer)
		clearTimeout(this.pendingRequest.timer);
	this.pendingRequest = null;
};

Spry.Data.DataSet.prototype.getRowCount = function(unfiltered)
{
	var rows = this.getData(unfiltered);
	return rows ? rows.length : 0;
};

Spry.Data.DataSet.prototype.getRowByID = function(rowID)
{
	if (!this.data)
		return null;
	return this.dataHash[rowID];
};

Spry.Data.DataSet.prototype.getRowByRowNumber = function(rowNumber, unfiltered)
{
	var rows = this.getData(unfiltered);
	if (rows && rowNumber >= 0 && rowNumber < rows.length)
		return rows[rowNumber];
	return null;
};

Spry.Data.DataSet.prototype.getCurrentRow = function()
{
	return this.getRowByID(this.curRowID);
};

Spry.Data.DataSet.prototype.setCurrentRow = function(rowID)
{
	if (this.curRowID == rowID)
		return;

	var nData = { oldRowID: this.curRowID, newRowID: rowID };
	this.curRowID = rowID;
	this.notifyObservers("onCurrentRowChanged", nData);
};

Spry.Data.DataSet.prototype.getRowNumber = function(row, unfiltered)
{
	if (row)
	{
		var rows = this.getData(unfiltered);
		if (rows && rows.length)
		{
			var numRows = rows.length;
			for (var i = 0; i < numRows; i++)
			{
				if (rows[i] == row)
					return i;
			}
		}
	}
	return -1;
};

Spry.Data.DataSet.prototype.getCurrentRowNumber = function()
{
	return this.getRowNumber(this.getCurrentRow());
};

Spry.Data.DataSet.prototype.getCurrentRowID = function()
{
	return this.curRowID;
};

Spry.Data.DataSet.prototype.setCurrentRowNumber = function(rowNumber)
{
	if (!this.data || rowNumber >= this.data.length)
	{
		Spry.Debug.trace("Invalid row number: " + rowNumber + "\n");
		return;
	}

	var rowID = this.data[rowNumber]["ds_RowID"];

	if (rowID == undefined || this.curRowID == rowID)
		return;

	this.setCurrentRow(rowID);
};

Spry.Data.DataSet.prototype.findRowsWithColumnValues = function(valueObj, firstMatchOnly, unfiltered)
{
	var results = [];
	var rows = this.getData(unfiltered);
	if (rows)
	{
		var numRows = rows.length;
		for (var i = 0; i < numRows; i++)
		{
			var row = rows[i];
			var matched = true;

			for (var colName in valueObj)
			{
				if (valueObj[colName] != row[colName])
				{
					matched = false;
					break;
				}
			}


			if (matched)
			{
				if (firstMatchOnly)
					return row;
				results.push(row);
			}
		}
	}

	return firstMatchOnly ? null : results;
};

Spry.Data.DataSet.prototype.setColumnType = function(columnNames, columnType)
{
	if (columnNames)
	{
		if (typeof columnNames == "string")
			columnNames = [ columnNames ];
		for (var i = 0; i < columnNames.length; i++)
			this.columnTypes[columnNames[i]] = columnType;
	}
};

Spry.Data.DataSet.prototype.getColumnType = function(columnName)
{
	if (this.columnTypes[columnName])
		return this.columnTypes[columnName];
	return "string";
};

Spry.Data.DataSet.prototype.applyColumnTypes = function()
{
	var rows = this.getData(true);
	var numRows = rows.length;
	var colNames = [];

	if (numRows < 1)
		return;

	for (var cname in this.columnTypes)
	{
		var ctype = this.columnTypes[cname];
		if (ctype != "string")
		{
			for (var i = 0; i < numRows; i++)
			{
				var row = rows[i];
				var val = row[cname];
				if (val != undefined)
				{
					if (ctype == "number")
						row[cname] = new Number(val);
					else if (ctype == "html")
						row[cname] = Spry.Utils.decodeEntities(val);
				}
			}
		}
	}
};

Spry.Data.DataSet.prototype.distinct = function(columnNames)
{
	if (this.data)
	{
		var oldData = this.data;
		this.data = [];
		this.dataHash = {};
		var dataChanged = false;

		var alreadySeenHash = {};
		var i = 0;

		var keys = [];

		if (typeof columnNames == "string")
			keys = [columnNames];
		else if (columnNames)
			keys = columnNames;
		else
			for (var recField in oldData[0])
				keys[i++] = recField;

		for (var i = 0; i < oldData.length; i++)
		{
			var rec = oldData[i];
			var hashStr = "";
			for (var j=0; j < keys.length; j++)
			{
				recField = keys[j];
				if (recField != "ds_RowID")
				{
					if (hashStr)
						hashStr += ",";
					hashStr += recField + ":" + "\"" + rec[recField] + "\"";
				}
			}
			if (!alreadySeenHash[hashStr])
			{
				this.data.push(rec);
				this.dataHash[rec['ds_RowID']] = rec;
				alreadySeenHash[hashStr] = true;
			}
			else
				dataChanged = true;
		}
		if (dataChanged)
			this.notifyObservers('onDataChanged');
	}
};

Spry.Data.DataSet.prototype.getSortColumn = function() {
	return (this.lastSortColumns && this.lastSortColumns.length > 0) ? this.lastSortColumns[0] : "";
};

Spry.Data.DataSet.prototype.getSortOrder = function() {
	return this.lastSortOrder ? this.lastSortOrder : "";
};

Spry.Data.DataSet.prototype.sort = function(columnNames, sortOrder)
{
	// columnNames can be either the name of a column to
	// sort on, or an array of column names, but it can't be
	// null/undefined.

	if (!columnNames)
		return;

	// If only one column name was specified for sorting, do a
	// secondary sort on ds_RowID so we get a stable sort order.

	if (typeof columnNames == "string")
		columnNames = [ columnNames, "ds_RowID" ];
	else if (columnNames.length < 2 && columnNames[0] != "ds_RowID")
		columnNames.push("ds_RowID");

	if (!sortOrder)
		sortOrder = "toggle";

	if (sortOrder == "toggle")
	{
		if (this.lastSortColumns.length > 0 && this.lastSortColumns[0] == columnNames[0] && this.lastSortOrder == "ascending")
			sortOrder = "descending";
		else
			sortOrder = "ascending";
	}

	if (sortOrder != "ascending" && sortOrder != "descending")
	{
		Spry.Debug.reportError("Invalid sort order type specified: " + sortOrder + "\n");
		return;
	}

	var nData = {
		oldSortColumns: this.lastSortColumns,
		oldSortOrder: this.lastSortOrder,
		newSortColumns: columnNames,
		newSortOrder: sortOrder
	};
	this.notifyObservers("onPreSort", nData);

	var cname = columnNames[columnNames.length - 1];
	var sortfunc = Spry.Data.DataSet.prototype.sort.getSortFunc(cname, this.getColumnType(cname), sortOrder);

	for (var i = columnNames.length - 2; i >= 0; i--)
	{
		cname = columnNames[i];
		sortfunc = Spry.Data.DataSet.prototype.sort.buildSecondarySortFunc(Spry.Data.DataSet.prototype.sort.getSortFunc(cname, this.getColumnType(cname), sortOrder), sortfunc);
	}

	if (this.unfilteredData)
	{
		this.unfilteredData.sort(sortfunc);
		if (this.filterFunc)
			this.filter(this.filterFunc, true);
	}
	else
		this.data.sort(sortfunc);

	this.lastSortColumns = columnNames.slice(0); // Copy the array.
	this.lastSortOrder = sortOrder;

	this.notifyObservers("onPostSort", nData);
};

Spry.Data.DataSet.prototype.sort.getSortFunc = function(prop, type, order)
{
	var sortfunc = null;
	if (type == "number")
	{
		if (order == "ascending")
			sortfunc = function(a, b)
			{
				a = a[prop]; b = b[prop];
				if (a == undefined || b == undefined)
					return (a == b) ? 0 : (a ? 1 : -1);
				return a-b;
			};
		else // order == "descending"
			sortfunc = function(a, b)
			{
				a = a[prop]; b = b[prop];
				if (a == undefined || b == undefined)
					return (a == b) ? 0 : (a ? -1 : 1);
				return b-a;
			};
	}
	else if (type == "date")
	{
		if (order == "ascending")
			sortfunc = function(a, b)
			{
				var dA = a[prop];
				var dB = b[prop];
				dA = dA ? (new Date(dA)) : 0;
				dB = dB ? (new Date(dB)) : 0;
				return dA - dB;
			};
		else // order == "descending"
			sortfunc = function(a, b)
			{
				var dA = a[prop];
				var dB = b[prop];
				dA = dA ? (new Date(dA)) : 0;
				dB = dB ? (new Date(dB)) : 0;
				return dB - dA;
			};
	}
	else // type == "string" || type == "html"
	{
		if (order == "ascending")
			sortfunc = function(a, b){
				a = a[prop];
				b = b[prop];
				if (a == undefined || b == undefined)
					return (a == b) ? 0 : (a ? 1 : -1);
				var tA = a.toString();
				var tB = b.toString();
				var tA_l = tA.toLowerCase();
				var tB_l = tB.toLowerCase();
				var min_len = tA.length > tB.length ? tB.length : tA.length;

				for (var i=0; i < min_len; i++)
				{
					var a_l_c = tA_l.charAt(i);
					var b_l_c = tB_l.charAt(i);
					var a_c = tA.charAt(i);
					var b_c = tB.charAt(i);
					if (a_l_c > b_l_c)
						return 1;
					else if (a_l_c < b_l_c)
						return -1;
					else if (a_c > b_c)
						return 1;
					else if (a_c < b_c)
						return -1;
				}
				if(tA.length == tB.length)
					return 0;
				else if (tA.length > tB.length)
					return 1;
				return -1;
			};
		else // order == "descending"
			sortfunc = function(a, b){
				a = a[prop];
				b = b[prop];
				if (a == undefined || b == undefined)
					return (a == b) ? 0 : (a ? -1 : 1);
				var tA = a.toString();
				var tB = b.toString();
				var tA_l = tA.toLowerCase();
				var tB_l = tB.toLowerCase();
				var min_len = tA.length > tB.length ? tB.length : tA.length;
				for (var i=0; i < min_len; i++)
				{
					var a_l_c = tA_l.charAt(i);
					var b_l_c = tB_l.charAt(i);
					var a_c = tA.charAt(i);
					var b_c = tB.charAt(i);
					if (a_l_c > b_l_c)
						return -1;
					else if (a_l_c < b_l_c)
						return 1;
					else if (a_c > b_c)
						return -1;
					else if (a_c < b_c)
						return 1;
				}
				if(tA.length == tB.length)
					return 0;
				else if (tA.length > tB.length)
					return -1;
				return 1;
			};
	}

	return sortfunc;
};

Spry.Data.DataSet.prototype.sort.buildSecondarySortFunc = function(funcA, funcB)
{
	return function(a, b)
	{
		var ret = funcA(a, b);
		if (ret == 0)
			ret = funcB(a, b);
		return ret;
	};
};

Spry.Data.DataSet.prototype.filterData = function(filterFunc, filterOnly)
{
	// This is a destructive filter function.

	var dataChanged = false;

	if (!filterFunc)
	{
		// Caller wants to remove the filter.

		this.filterDataFunc = null;
		dataChanged = true;
	}
	else
	{
		this.filterDataFunc = filterFunc;

		if (this.dataWasLoaded && ((this.unfilteredData && this.unfilteredData.length) || (this.data && this.data.length)))
		{
			if (this.unfilteredData)
			{
				this.data = this.unfilteredData;
				this.unfilteredData = null;
			}

			var oldData = this.data;
			this.data = [];
			this.dataHash = {};

			for (var i = 0; i < oldData.length; i++)
			{
				var newRow = filterFunc(this, oldData[i], i);
				if (newRow)
				{
					this.data.push(newRow);
					this.dataHash[newRow["ds_RowID"]] = newRow;
				}
			}

			dataChanged = true;
		}
	}

	if (dataChanged)
	{
		if (!filterOnly)
		{
			this.disableNotifications();
			if (this.filterFunc)
				this.filter(this.filterFunc, true);
			this.enableNotifications();
		}

		this.notifyObservers("onDataChanged");
	}
};

Spry.Data.DataSet.prototype.filter = function(filterFunc, filterOnly)
{
	// This is a non-destructive filter function.

	var dataChanged = false;

	if (!filterFunc)
	{
		if (this.filterFunc && this.unfilteredData)
		{
			// Caller wants to remove the filter. Restore the unfiltered
			// data and trigger a data changed notification.

			this.data = this.unfilteredData;
			this.unfilteredData = null;
			this.filterFunc = null;
			dataChanged = true;
		}
	}
	else
	{
		this.filterFunc = filterFunc;

		if (this.dataWasLoaded && (this.unfilteredData || (this.data && this.data.length)))
		{
			if (!this.unfilteredData)
				this.unfilteredData = this.data;

			var udata = this.unfilteredData;
			this.data = [];

			for (var i = 0; i < udata.length; i++)
			{
				var newRow = filterFunc(this, udata[i], i);

				if (newRow)
					this.data.push(newRow);
			}

			dataChanged = true;
		}
	}

	if (dataChanged)
		this.notifyObservers("onDataChanged");
};

Spry.Data.DataSet.prototype.startLoadInterval = function(interval)
{
	this.stopLoadInterval();
	if (interval > 0)
	{
		var self = this;
		this.loadInterval = interval;
		this.loadIntervalID = setInterval(function() { self.loadData(); }, interval);
	}
};

Spry.Data.DataSet.prototype.stopLoadInterval = function()
{
	if (this.loadIntervalID)
		clearInterval(this.loadIntervalID);
	this.loadInterval = 0;
	this.loadIntervalID = null;
};

Spry.Data.DataSet.nextDataSetID = 0;

//////////////////////////////////////////////////////////////////////
//
// Spry.Data.HTTPSourceDataSet
// base class for any DataSet that uses external
//
//////////////////////////////////////////////////////////////////////

Spry.Data.HTTPSourceDataSet = function(dataSetURL, dataSetOptions)
{
	// Call the constructor for our DataSet base class so that
	// our base class properties get defined. We'll call setOptions
	// manually after we set up our HTTPSourceDataSet properties.

	Spry.Data.DataSet.call(this);

	// HTTPSourceDataSet Properties:

	this.url = dataSetURL;
	this.dataSetsForDataRefStrings = new Array;
	this.hasDataRefStrings = false;
	this.useCache = true;

	this.setRequestInfo(dataSetOptions, true);

	Spry.Utils.setOptions(this, dataSetOptions, true);

	this.recalculateDataSetDependencies();

	if (this.loadInterval > 0)
		this.startLoadInterval(this.loadInterval);
}; // End of Spry.Data.HTTPSourceDataSet() constructor.

Spry.Data.HTTPSourceDataSet.prototype = new Spry.Data.DataSet();
Spry.Data.HTTPSourceDataSet.prototype.constructor = Spry.Data.HTTPSourceDataSet;

Spry.Data.HTTPSourceDataSet.prototype.setRequestInfo = function(requestInfo, undefineRequestProps)
{
	// Create a loadURL request object to store any load options
	// the caller specified. We'll fill in the URL at the last minute
	// before we make the actual load request because our URL needs
	// to be processed at the last possible minute in case it contains
	// data references.

	this.requestInfo = new Spry.Utils.loadURL.Request();
	this.requestInfo.extractRequestOptions(requestInfo, undefineRequestProps);

	// If the caller wants to use "POST" to fetch the data, but didn't
	// provide the content type, default to x-www-form-urlencoded.

	if (this.requestInfo.method == "POST")
	{
		if (!this.requestInfo.headers)
			this.requestInfo.headers = {};
		if (!this.requestInfo.headers['Content-Type'])
			this.requestInfo.headers['Content-Type'] = "application/x-www-form-urlencoded; charset=UTF-8";
	}
};

Spry.Data.HTTPSourceDataSet.prototype.recalculateDataSetDependencies = function()
{
	this.hasDataRefStrings = false;

	// Clear all old callbacks that may have been registered.

	var i = 0;
	for (i = 0; i < this.dataSetsForDataRefStrings.length; i++)
	{
		var ds = this.dataSetsForDataRefStrings[i];
		if (ds)
			ds.removeObserver(this);
	}

	// Now run through the strings that may contain data references and figure
	// out what data sets they require. Note that the data references in these
	// strings must be fully qualified with a data set name. (ex: {dsDataSetName::columnName})

	this.dataSetsForDataRefStrings = new Array();

	var regionStrs = this.getDataRefStrings();

	var dsCount = 0;

	for (var n = 0; n < regionStrs.length; n++)
	{
		var tokens = Spry.Data.Region.getTokensFromStr(regionStrs[n]);

		for (i = 0; tokens && i < tokens.length; i++)
		{
			if (tokens[i].search(/{[^}:]+::[^}]+}/) != -1)
			{
				var dsName = tokens[i].replace(/^\{|::.*\}/g, "");
				var ds = null;
				if (!this.dataSetsForDataRefStrings[dsName])
				{
					ds = Spry.Data.getDataSetByName(dsName);
					if (dsName && ds)
					{
						// The dataSetsForDataRefStrings array serves as both an
						// array of data sets and a hash lookup by name.

						this.dataSetsForDataRefStrings[dsName] = ds;
						this.dataSetsForDataRefStrings[dsCount++] = ds;
						this.hasDataRefStrings = true;
					}
				}
			}
		}
	}

	// Set up observers on any data sets our URL depends on.

	for (i = 0; i < this.dataSetsForDataRefStrings.length; i++)
	{
		var ds = this.dataSetsForDataRefStrings[i];
		ds.addObserver(this);
	}
};

Spry.Data.HTTPSourceDataSet.prototype.getDataRefStrings = function()
{
	var strArr = [];
	if (this.url) strArr.push(this.url);
	if (this.requestInfo && this.requestInfo.postData) strArr.push(this.requestInfo.postData);
	return strArr;
};

Spry.Data.HTTPSourceDataSet.prototype.attemptLoadData = function()
{
	// We only want to trigger a load when all of our data sets have data!
	for (var i = 0; i < this.dataSetsForDataRefStrings.length; i++)
	{
		var ds = this.dataSetsForDataRefStrings[i];
		if (ds.getLoadDataRequestIsPending() || !ds.getDataWasLoaded())
			return;
	}

	this.loadData();
};

Spry.Data.HTTPSourceDataSet.prototype.onCurrentRowChanged = function(ds, data)
{
	this.attemptLoadData();
};

Spry.Data.HTTPSourceDataSet.prototype.onPostSort = function(ds, data)
{
	this.attemptLoadData();
};

Spry.Data.HTTPSourceDataSet.prototype.onDataChanged = function(ds, data)
{
	this.attemptLoadData();
};

Spry.Data.HTTPSourceDataSet.prototype.loadData = function()
{
	if (!this.url)
		return;

	this.cancelLoadData();

	var url = this.url;
	var postData = this.requestInfo.postData;

	if (this.hasDataRefStrings)
	{
		var allDataSetsReady = true;

		for (var i = 0; i < this.dataSetsForDataRefStrings.length; i++)
		{
			var ds = this.dataSetsForDataRefStrings[i];
			if (ds.getLoadDataRequestIsPending())
				allDataSetsReady = false;
			else if (!ds.getDataWasLoaded())
			{
				// Kick off the load of this data set!
				ds.loadData();
				allDataSetsReady = false;
			}
		}

		// If our data sets aren't ready, just return. We'll
		// get called back to load our data when they are all
		// done.

		if (!allDataSetsReady)
			return;

		url = Spry.Data.Region.processDataRefString(null, this.url, this.dataSetsForDataRefStrings);
		if (!url)
			return;

		if (postData && (typeof postData) == "string")
			postData = Spry.Data.Region.processDataRefString(null, postData, this.dataSetsForDataRefStrings);
	}

	this.notifyObservers("onPreLoad");

	this.data = null;
	this.dataWasLoaded = false;
	this.unfilteredData = null;
	this.dataHash = null;
	this.curRowID = 0;

	// At this point the url should've been processed if it contained any
	// data references. Set the url of the requestInfo structure and pass it
	// to LoadManager.loadData().

	var req = this.requestInfo.clone();
	req.url = url;
	req.postData = postData;

	this.pendingRequest = new Object;
	this.pendingRequest.data = Spry.Data.HTTPSourceDataSet.LoadManager.loadData(req, this, this.useCache);
};

Spry.Data.HTTPSourceDataSet.prototype.cancelLoadData = function()
{
	if (this.pendingRequest)
	{
		Spry.Data.HTTPSourceDataSet.LoadManager.cancelLoadData(this.pendingRequest.data, this);
		this.pendingRequest = null;
	}
};

Spry.Data.HTTPSourceDataSet.prototype.getURL = function() { return this.url; };
Spry.Data.HTTPSourceDataSet.prototype.setURL = function(url, requestOptions)
{
	if (this.url == url)
	{
		// The urls match so we may not have to do anything, but
		// before we bail early, check to see if the method and
		// postData that was last used was the same. If there is a
		// difference, we need to process the new URL.

		if (!requestOptions || (this.requestInfo.method == requestOptions.method && (requestOptions.method != "POST" || this.requestInfo.postData == requestOptions.postData)))
			return;
	}

	this.url = url;

	this.setRequestInfo(requestOptions);

	this.cancelLoadData();
	this.recalculateDataSetDependencies();
	this.dataWasLoaded = false;
};

Spry.Data.HTTPSourceDataSet.prototype.setDataFromDoc = function(rawDataDoc)
{
	this.pendingRequest = null;

	this.loadDataIntoDataSet(rawDataDoc);
	this.applyColumnTypes();

	this.disableNotifications();
	this.filterAndSortData();
	this.enableNotifications();

	this.notifyObservers("onPostLoad");
	this.notifyObservers("onDataChanged");
};

Spry.Data.HTTPSourceDataSet.prototype.loadDataIntoDataSet = function(rawDataDoc)
{
	// this method needs to be overwritten by the descendent classes;
	// internal data structures (data & dataHash) have to load data from the source document (ResponseText | ResponseDoc);

	this.dataHash = new Object;
	this.data = new Array;
	this.dataWasLoaded = true;
};

Spry.Data.HTTPSourceDataSet.prototype.xhRequestProcessor = function(xhRequest)
{
	// This method needs to be overwritten by the descendent classes if other objects (like responseXML)
	// are going to be used as a data source
	// This implementation returns the responseText from xhRequest

	var resp = xhRequest.responseText;

	if (xhRequest.status == 200 || xhRequest.status == 0)
		return resp;
	return null;
};

Spry.Data.HTTPSourceDataSet.prototype.sessionExpiredChecker = function(req)
{
	if (req.xhRequest.responseText == 'session expired')
		return true;
	return false;
};

Spry.Data.HTTPSourceDataSet.prototype.setSessionExpiredChecker = function(checker)
{
	this.sessionExpiredChecker = checker;
};


Spry.Data.HTTPSourceDataSet.prototype.onRequestResponse = function(cachedRequest, req)
{
	this.setDataFromDoc(cachedRequest.rawData);
};

Spry.Data.HTTPSourceDataSet.prototype.onRequestError = function(cachedRequest, req)
{
	this.notifyObservers("onLoadError", req);
	// Spry.Debug.reportError("Spry.Data.HTTPSourceDataSet.LoadManager.CachedRequest.loadDataCallback(" + req.xhRequest.status + ") failed to load: " + req.url + "\n");
};

Spry.Data.HTTPSourceDataSet.prototype.onRequestSessionExpired = function(cachedRequest, req)
{
	this.notifyObservers("onSessionExpired", req);
	//Spry.Debug.reportError("Spry.Data.HTTPSourceDataSet.LoadManager.CachedRequest.loadDataCallback(" + req.xhRequest.status + ") failed to load: " + req.url + "\n");
};


Spry.Data.HTTPSourceDataSet.LoadManager = {};
Spry.Data.HTTPSourceDataSet.LoadManager.cache = [];

Spry.Data.HTTPSourceDataSet.LoadManager.CachedRequest = function(reqInfo, xhRequestProcessor, sessionExpiredChecker)
{
	Spry.Utils.Notifier.call(this);

	this.reqInfo = reqInfo;
	this.rawData = null;
	this.timer = null;
	this.state = Spry.Data.HTTPSourceDataSet.LoadManager.CachedRequest.NOT_LOADED;
	this.xhRequestProcessor = xhRequestProcessor;
	this.sessionExpiredChecker = sessionExpiredChecker;
};

Spry.Data.HTTPSourceDataSet.LoadManager.CachedRequest.prototype = new Spry.Utils.Notifier();
Spry.Data.HTTPSourceDataSet.LoadManager.CachedRequest.prototype.constructor = Spry.Data.HTTPSourceDataSet.LoadManager.CachedRequest;

Spry.Data.HTTPSourceDataSet.LoadManager.CachedRequest.NOT_LOADED      = 1;
Spry.Data.HTTPSourceDataSet.LoadManager.CachedRequest.LOAD_REQUESTED  = 2;
Spry.Data.HTTPSourceDataSet.LoadManager.CachedRequest.LOAD_FAILED     = 3;
Spry.Data.HTTPSourceDataSet.LoadManager.CachedRequest.LOAD_SUCCESSFUL = 4;

Spry.Data.HTTPSourceDataSet.LoadManager.CachedRequest.prototype.loadDataCallback = function(req)
{
	if (req.xhRequest.readyState != 4)
		return;

	var rawData = null;
	if (this.xhRequestProcessor) rawData = this.xhRequestProcessor(req.xhRequest);

	if (this.sessionExpiredChecker)
	{
		Spry.Utils.setOptions(req, {'rawData': rawData}, false);
		if (this.sessionExpiredChecker(req))
		{
			this.state = Spry.Data.HTTPSourceDataSet.LoadManager.CachedRequest.LOAD_FAILED;
			this.notifyObservers("onRequestSessionExpired", req);
			this.observers.length = 0;
			return;
		}
	}

	if (!rawData)
	{
		this.state = Spry.Data.HTTPSourceDataSet.LoadManager.CachedRequest.LOAD_FAILED;
		this.notifyObservers("onRequestError", req);
		this.observers.length = 0; // Clear the observers list.
		return;
	}

	this.rawData = rawData;
	this.state = Spry.Data.HTTPSourceDataSet.LoadManager.CachedRequest.LOAD_SUCCESSFUL;

	// Notify all of the cached request's observers!
	this.notifyObservers("onRequestResponse", req);

	// Clear the observers list.
	this.observers.length = 0;
};

Spry.Data.HTTPSourceDataSet.LoadManager.CachedRequest.prototype.loadData = function()
{
	// IE will synchronously fire our loadDataCallback() during the call
	// to an async Spry.Utils.loadURL() if the data for the url is already
	// in the browser's local cache. This can wreak havoc with complicated master/detail
	// regions that use data sets that have master/detail relationships with other
	// data sets. Our data set logic already handles async data loading nicely so we
	// use a timer to fire off the async Spry.Utils.loadURL() call to insure that any
	// data loading happens asynchronously after this function is finished.

	var self = this;
	this.cancelLoadData();
	this.rawData = null;
	this.state = Spry.Data.HTTPSourceDataSet.LoadManager.CachedRequest.LOAD_REQUESTED;

	var reqInfo = this.reqInfo.clone();
	reqInfo.successCallback = function(req) { self.loadDataCallback(req); };
	reqInfo.errorCallback = reqInfo.successCallback;

	this.timer = setTimeout(function()
	{
		self.timer = null;
		Spry.Utils.loadURL(reqInfo.method, reqInfo.url, reqInfo.async, reqInfo.successCallback, reqInfo);
	}, 0);
};

Spry.Data.HTTPSourceDataSet.LoadManager.CachedRequest.prototype.cancelLoadData = function()
{
	if (this.state == Spry.Data.HTTPSourceDataSet.LoadManager.CachedRequest.LOAD_REQUESTED)
	{
		if (this.timer)
		{
			this.timer.clearTimeout();
			this.timer = null;
		}

		this.rawData = null;
		this.state = Spry.Data.HTTPSourceDataSet.LoadManager.CachedRequest.NOT_LOADED;
	}
};

Spry.Data.HTTPSourceDataSet.LoadManager.getCacheKey = function(reqInfo)
{
	return reqInfo.method + "::" + reqInfo.url + "::" + reqInfo.postData + "::" + reqInfo.username;
};

Spry.Data.HTTPSourceDataSet.LoadManager.loadData = function(reqInfo, ds, useCache)
{
	if (!reqInfo)
		return null;

	var cacheObj = null;
	var cacheKey = null;

	if (useCache)
	{
		cacheKey = Spry.Data.HTTPSourceDataSet.LoadManager.getCacheKey(reqInfo);
		cacheObj = Spry.Data.HTTPSourceDataSet.LoadManager.cache[cacheKey];
	}

	if (cacheObj)
	{
		if (cacheObj.state == Spry.Data.HTTPSourceDataSet.LoadManager.CachedRequest.LOAD_REQUESTED)
		{
			if (ds)
				cacheObj.addObserver(ds);
			return cacheObj;
		}
		else if (cacheObj.state == Spry.Data.HTTPSourceDataSet.LoadManager.CachedRequest.LOAD_SUCCESSFUL)
		{
			// Data is already cached so if we have a data set, trigger an async call
			// that tells it to load its data.
			if (ds)
				setTimeout(function() { ds.setDataFromDoc(cacheObj.rawData); }, 0);
			return cacheObj;
		}
	}

	// We're either loading this url for the first time, or an error occurred when
	// we last tried to load it, or the caller requested a forced load.

	if (!cacheObj)
	{
		cacheObj = new Spry.Data.HTTPSourceDataSet.LoadManager.CachedRequest(reqInfo, (ds ? ds.xhRequestProcessor : null), (ds ? ds.sessionExpiredChecker : null));

		if (useCache)
		{
			Spry.Data.HTTPSourceDataSet.LoadManager.cache[cacheKey] = cacheObj;

			// Add an observer that will remove the cacheObj from the cache
			// if there is a load request failure.
			cacheObj.addObserver({ onRequestError: function() { Spry.Data.HTTPSourceDataSet.LoadManager.cache[cacheKey] = undefined; }});
		}
	}

	if (ds)
		cacheObj.addObserver(ds);

	cacheObj.loadData();

	return cacheObj;
};

Spry.Data.HTTPSourceDataSet.LoadManager.cancelLoadData = function(cacheObj, ds)
{
	if (cacheObj)
	{
		if (ds)
			cacheObj.removeObserver(ds);
		else
			cacheObj.cancelLoadData();
	}
};

//////////////////////////////////////////////////////////////////////
//
// Spry.Data.XMLDataSet
//
//////////////////////////////////////////////////////////////////////

Spry.Data.XMLDataSet = function(dataSetURL, dataSetPath, dataSetOptions)
{
	// Call the constructor for our HTTPSourceDataSet base class so that
	// our base class properties get defined.

	this.xpath = dataSetPath;
	this.doc = null;
	this.subPaths = [];
	this.entityEncodeStrings = true;

	Spry.Data.HTTPSourceDataSet.call(this, dataSetURL, dataSetOptions);

	// Callers are allowed to pass either a string, an object or an array of
	// strings and/or objects for the 'subPaths' option, so make sure we normalize
	// the subPaths value to be an array.

	var jwType = typeof this.subPaths;
	if (jwType == "string" || (jwType == "object" && this.subPaths.constructor != Array))
		this.subPaths = [ this.subPaths ];
}; // End of Spry.Data.XMLDataSet() constructor.

Spry.Data.XMLDataSet.prototype = new Spry.Data.HTTPSourceDataSet();
Spry.Data.XMLDataSet.prototype.constructor = Spry.Data.XMLDataSet;


Spry.Data.XMLDataSet.prototype.getDataRefStrings = function()
{
	var strArr = [];
	if (this.url) strArr.push(this.url);
	if (this.xpath) strArr.push(this.xpath);
	if (this.requestInfo && this.requestInfo.postData) strArr.push(this.requestInfo.postData);
	return strArr;
};

Spry.Data.XMLDataSet.prototype.getDocument = function() { return this.doc; };
Spry.Data.XMLDataSet.prototype.getXPath = function() { return this.xpath; };
Spry.Data.XMLDataSet.prototype.setXPath = function(path)
{
	if (this.xpath != path)
	{
		this.xpath = path;
		if (this.dataWasLoaded && this.doc)
		{
			this.notifyObservers("onPreLoad");
			this.setDataFromDoc(this.doc);
		}
	}
};

Spry.Data.XMLDataSet.nodeContainsElementNode = function(node)
{
	if (node)
	{
		node = node.firstChild;

		while (node)
		{
			if (node.nodeType == 1 /* Node.ELEMENT_NODE */)
				return true;

			node = node.nextSibling;
		}
	}
	return false;
};

Spry.Data.XMLDataSet.getNodeText = function(node, encodeText, encodeCData)
{
	var txt = "";

	if (!node)
		return;

	try
	{
		var child = node.firstChild;

		while (child)
		{
			try
			{
				if (child.nodeType == 3 /* TEXT_NODE */)
					txt += encodeText ? Spry.Utils.encodeEntities(child.data) : child.data;
				else if (child.nodeType == 4 /* CDATA_SECTION_NODE */)
					txt += encodeCData ? Spry.Utils.encodeEntities(child.data) : child.data;
			} catch (e) { Spry.Debug.reportError("Spry.Data.XMLDataSet.getNodeText() exception caught: " + e + "\n"); }

			child = child.nextSibling;
		}
	}
	catch (e) { Spry.Debug.reportError("Spry.Data.XMLDataSet.getNodeText() exception caught: " + e + "\n"); }

	return txt;
};

Spry.Data.XMLDataSet.createObjectForNode = function(node, encodeText, encodeCData)
{
	if (!node)
		return null;

	var obj = new Object();
	var i = 0;
	var attr = null;

	try
	{
		for (i = 0; i < node.attributes.length; i++)
		{
			attr = node.attributes[i];
			if (attr && attr.nodeType == 2 /* Node.ATTRIBUTE_NODE */)
				obj["@" + attr.name] = attr.value;
		}
	}
	catch (e)
	{
		Spry.Debug.reportError("Spry.Data.XMLDataSet.createObjectForNode() caught exception while accessing attributes: " + e + "\n");
	}

	var child = node.firstChild;

	if (child && !child.nextSibling && child.nodeType != 1 /* Node.ELEMENT_NODE */)
	{
		// We have a single child and it's not an element. It must
		// be the text value for this node. Add it to the record set and
		// give it the column the same name as the node.

		obj[node.nodeName] = Spry.Data.XMLDataSet.getNodeText(node, encodeText, encodeCData);
	}

	while (child)
	{
		// Add the text value for each child element. Note that
		// We skip elements that have element children (sub-elements)
		// because we don't handle multi-level data sets right now.

		if (child.nodeType == 1 /* Node.ELEMENT_NODE */)
		{
			if (!Spry.Data.XMLDataSet.nodeContainsElementNode(child))
			{
				obj[child.nodeName] = Spry.Data.XMLDataSet.getNodeText(child, encodeText, encodeCData);

				// Now add properties for any attributes on the child. The property
				// name will be of the form "<child.nodeName>/@<attr.name>".
				try
				{
					var namePrefix = child.nodeName + "/@";

					for (i = 0; i < child.attributes.length; i++)
					{
						attr = child.attributes[i];
						if (attr && attr.nodeType == 2 /* Node.ATTRIBUTE_NODE */)
							obj[namePrefix + attr.name] = attr.value;
					}
				}
				catch (e)
				{
					Spry.Debug.reportError("Spry.Data.XMLDataSet.createObjectForNode() caught exception while accessing attributes: " + e + "\n");
				}
			}
		}

		child = child.nextSibling;
	}

	return obj;
};

Spry.Data.XMLDataSet.getRecordSetFromXMLDoc = function(xmlDoc, path, suppressColumns, entityEncodeStrings)
{
	if (!xmlDoc || !path)
		return null;

	var recordSet = new Object();
	recordSet.xmlDoc = xmlDoc;
	recordSet.xmlPath = path;
	recordSet.dataHash = new Object;
	recordSet.data = new Array;
	recordSet.getData = function() { return this.data; };

	// Use the XPath library to find the nodes that will
	// make up our data set. The result should be an array
	// of subtrees that we need to flatten.

	var ctx = new ExprContext(xmlDoc);
	var pathExpr = xpathParse(path);
	var e = pathExpr.evaluate(ctx);

	// XXX: Note that we should check the result type of the evaluation
	// just in case it's a boolean, string, or number value instead of
	// a node set.

	var nodeArray = e.nodeSetValue();

	var isDOMNodeArray = true;

	if (nodeArray && nodeArray.length > 0)
		isDOMNodeArray = nodeArray[0].nodeType != 2 /* Node.ATTRIBUTE_NODE */;

	var nextID = 0;

	var encodeText = true;
	var encodeCData = false;
	if (typeof entityEncodeStrings == "boolean")
		encodeText = encodeCData = entityEncodeStrings;

	// We now have the set of nodes that make up our data set
	// so process each one.

	for (var i = 0; i < nodeArray.length; i++)
	{
		var rowObj = null;

		if (suppressColumns)
			rowObj = new Object;
		else
		{
			if (isDOMNodeArray)
				rowObj = Spry.Data.XMLDataSet.createObjectForNode(nodeArray[i], encodeText, encodeCData);
			else // Must be a Node.ATTRIBUTE_NODE array.
			{
				rowObj = new Object;
				rowObj["@" + nodeArray[i].name] = nodeArray[i].value;
			}
		}

		if (rowObj)
		{
			// We want to make sure that every row has a unique ID and since we
			// we don't know which column, if any, in this recordSet is a unique
			// identifier, we generate a unique ID ourselves and store it under
			// the ds_RowID column in the row object.

			rowObj['ds_RowID'] = nextID++;
			rowObj['ds_XMLNode'] = nodeArray[i];
			recordSet.dataHash[rowObj['ds_RowID']] = rowObj;
			recordSet.data.push(rowObj);
		}
	}

	return recordSet;
};

Spry.Data.XMLDataSet.PathNode = function(path)
{
	this.path = path;
	this.subPaths = [];
	this.xpath = "";
};

Spry.Data.XMLDataSet.PathNode.prototype.addSubPath = function(path)
{
	var node = this.findSubPath(path);
	if (!node)
	{
		node = new Spry.Data.XMLDataSet.PathNode(path);
		this.subPaths.push(node);
	}
	return node;
};

Spry.Data.XMLDataSet.PathNode.prototype.findSubPath = function(path)
{
	var numSubPaths = this.subPaths.length;
	for (var i = 0; i < numSubPaths; i++)
	{
		var subPath = this.subPaths[i];
		if (path == subPath.path)
			return subPath;
	}
	return null;
};

Spry.Data.XMLDataSet.PathNode.prototype.consolidate = function()
{
	// This method recursively runs through the path tree and
	// tries to flatten any nodes that have no XPath and one child.
	// The flattening involves merging the parent's path component
	// with its child path component.

	var numSubPaths = this.subPaths.length;
	if (!this.xpath && numSubPaths == 1)
	{
		// Consolidate!
		var subPath = this.subPaths[0];
		this.path += ((subPath[0] != "/") ? "/" : "") + subPath.path;
		this.xpath = subPath.xpath;
		this.subPaths = subPath.subPaths;
		this.consolidate();
		return;
	}

	for (var i = 0; i < numSubPaths; i++)
		this.subPaths[i].consolidate();
};

/* This method is commented out so that it gets stripped when the file
   is minimized. Please do not remove this from the full version of the
   file! It is needed for debugging.

Spry.Data.XMLDataSet.PathNode.prototype.dump = function(indentStr)
{
	var didPre = false;
	var result = "";
	if (!indentStr)
	{
		indentStr = "";
		didPre = true;
		result = "<pre>";
	}
	result += indentStr + "<strong>" + this.path + "</strong>" + (this.xpath ? " <em>-- xpath(" + Spry.Utils.encodeEntities(this.xpath) + ")</em>" : "") + "\n";
	var numSubPaths = this.subPaths.length;
	indentStr += "    ";
	for (var i = 0; i < numSubPaths; i++)
		result += this.subPaths[i].dump(indentStr);
	if (didPre)
		result += "</pre>";
	return result;
};
*/

Spry.Data.XMLDataSet.prototype.convertXPathsToPathTree = function(xpathArray)
{
	var xpaLen = xpathArray.length;
	var root = new Spry.Data.XMLDataSet.PathNode("");

	for (var i = 0; i < xpaLen; i++)
	{
		// Convert any "//" in the XPath to our placeholder value.
		// We need to do that so they don't get removed when we split the
		// path into components.

		var xpath = xpathArray[i];
		var cleanXPath = xpath.replace(/\/\//g, "/__SPRYDS__");
		cleanXPath = cleanXPath.replace(/^\//, ""); // Strip any leading slash.
		var pathItems = cleanXPath.split(/\//);
		var pathItemsLen = pathItems.length;

		// Now add each path component to our tree.

		var node = root;
		for (var j = 0; j < pathItemsLen; j++)
		{
			// If this path component has a placeholder in it, convert it
			// back to a double slash.

			var path = pathItems[j].replace(/__SPRYDS__/, "//");
			node = node.addSubPath(path);
		}

		// Now add the full xpath to the node that represents the
		// last path component in our path.

		node.xpath = xpath;
	}

	// Now that we have a tree of nodes. Tell the root to consolidate
	// itself so we get a tree that is as flat as possible. This reduces
	// the number of XPaths we will have to flatten.

	root.consolidate();
	return root;
};

Spry.Data.XMLDataSet.prototype.flattenSubPaths = function(rs, subPaths)
{
	if (!rs || !subPaths)
		return;

	var numSubPaths = subPaths.length;
	if (numSubPaths < 1)
		return;

	var data = rs.data;
	var dataHash = {};

	// Convert all of the templated subPaths to XPaths with real values.
	// We also need a "cleaned" version of the XPath which contains no
	// expressions in it, so that we can pre-pend it to the column names
	// of any nested data we find.

	var xpathArray = [];
	var cleanedXPathArray = [];

	for (var i = 0; i < numSubPaths; i++)
	{
		// The elements of the subPaths array can be XPath strings,
		// or objects that describe a path with nested sub-paths below
		// it, so make sure we properly extract out the XPath to use.

		var subPath = subPaths[i];
		if (typeof subPath == "object")
			subPath = subPath.path;
		if (!subPath)
			subPath = "";

		// Convert any data references in the XPath to real values!

		xpathArray[i] = Spry.Data.Region.processDataRefString(null, subPath, this.dataSetsForDataRefStrings);

		// Create a clean version of the XPath by stripping out any
		// expressions it may contain.

		cleanedXPathArray[i] = xpathArray[i].replace(/\[.*\]/g, "");
	}

	// For each row of the base record set passed in, generate a flattened
	// recordset from each subPath, and then join the results with the base
	// row. The row from the base data set will be duplicated to match the
	// number of rows matched by the subPath. The results are then merged.

	var row;
	var numRows = data.length;
	var newData = [];

	// Iterate over each row of the base record set.

	for (var i = 0; i < numRows; i++)
	{
		row = data[i];
		var newRows = [ row ];

		// Iterate over every subPath passed into this function.

		for (var j = 0; j < numSubPaths; j++)
		{
			// Search for all nodes that match the given XPath underneath
			// the XML node for the base row and flatten the data into
			// a tabular recordset structure.

			var newRS = Spry.Data.XMLDataSet.getRecordSetFromXMLDoc(row.ds_XMLNode, xpathArray[j], (subPaths[j].xpath ? false : true), this.entityEncodeStrings);

			// If this subPath has additional subPaths beneath it,
			// flatten and join them with the recordset we just created.

			if (newRS && newRS.data && newRS.data.length)
			{
				if (typeof subPaths[j] == "object" && subPaths[j].subPaths)
				{
					// The subPaths property can be either an XPath string,
					// an Object describing a subPath and paths beneath it,
					// or an Array of XPath strings or objects. We need to
					// normalize these variations into an array to simplify
					// our processing.

					var sp = subPaths[j].subPaths;
					spType = typeof sp;
					if (spType == "string")
						sp = [ sp ];
					else if (spType == "object" && spType.constructor == Object)
						sp = [ sp ];

					// Now that we have a normalized array of sub paths, flatten
					// them and join them to the recordSet we just calculated.

					this.flattenSubPaths(newRS, sp);
				}

				var newRSData = newRS.data;
				var numRSRows = newRSData.length;

				var cleanedXPath = cleanedXPathArray[j] + "/";

				var numNewRows = newRows.length;
				var joinedRows = [];

				// Iterate over all rows in our newRows array. Note that the
				// contents of newRows changes after the execution of this
				// loop, allowing us to perform more joins when more than
				// one subPath is specified.

				for (var k = 0; k < numNewRows; k++)
				{
					var newRow = newRows[k];

					// Iterate over all rows in the record set generated
					// from the current subPath. We are going to create
					// m*n rows for the joined table, where m is the number
					// of rows in the newRows array, and n is the number of
					// rows in the current subPath recordset.

					for (var l = 0; l < numRSRows; l++)
					{
						// Create a new row that will house the join result.

						var newRowObj = new Object;
						var newRSRow = newRSData[l];

						// Copy the columns from the newRow into our row
						// object.

						for (prop in newRow)
							newRowObj[prop] = newRow[prop];

						// Copy the data from the current row of the record set
						// into our new row object, but make sure to store the
						// data in columns that have the subPath prepended to
						// it so that it doesn't collide with any columns from
						// the newRows row data.

						for (var prop in newRSRow)
						{
							// The new propery name will have the subPath used prepended to it.
							var newPropName = cleanedXPath + prop;

							// We need to handle the case where the tag name of the node matched
							// by the XPath has a value. In that specific case, the name of the
							// property should be the cleanedXPath itself. For example:
							//
							//	<employees>
							//		<employee>Bob</employee>
							//		<employee>Joe</employee>
							//	</employees>
							//
							// XPath: /employees/employee
							//
							// The property name that contains "Bob" and "Joe" will be "employee".
							// So in our new row, we need to call this column "/employees/employee"
							// instead of "/employees/employee/employee" which would be incorrect.

							if (cleanedXPath == (prop + "/") || cleanedXPath.search(new RegExp("\\/" + prop + "\\/$")) != -1)
								newPropName = cleanedXPathArray[j];

							// Copy the props to the new object using the new property name.

							newRowObj[newPropName] = newRSRow[prop];
						}

						// Now add this row to the array that tracks all of the new
						// rows we've just created.

						joinedRows.push(newRowObj);
					}
				}

				// Set the newRows array equal to our joinedRows we just created,
				// so that when we flatten the data for the next subPath, it gets
				// joined with our new set of rows.

				newRows = joinedRows;
			}
		}

		newData = newData.concat(newRows);
	}

	// Now that we have a new set of joined rows, we need to run through
	// all of the rows and make sure they all have a unique row ID and
	// rebuild our dataHash.

	data = newData;
	numRows = data.length;

	for (i = 0; i < numRows; i++)
	{
		row = data[i];
		row.ds_RowID = i;
		dataHash[row.ds_RowID] = row;
	}

	// We're all done, so stuff the new data and dataHash
	// back into the base recordSet.

	rs.data = data;
	rs.dataHash = dataHash;
};

Spry.Data.XMLDataSet.prototype.loadDataIntoDataSet = function(rawDataDoc)
{
	var rs = null;
	var mainXPath = Spry.Data.Region.processDataRefString(null, this.xpath, this.dataSetsForDataRefStrings);
	var subPaths = this.subPaths;
	var suppressColumns = false;

	if (this.subPaths && this.subPaths.length > 0)
	{
		// Some subPaths were specified. Convert any data references in each subPath
		// to real data. While we're at it, convert any subPaths that are relative
		// to our main XPath to absolute paths.

		var processedSubPaths = [];
		var numSubPaths = subPaths.length;
		for (var i = 0; i < numSubPaths; i++)
		{
			var subPathStr = Spry.Data.Region.processDataRefString(null, subPaths[i], this.dataSetsForDataRefStrings);
			if (subPathStr.charAt(0) != '/')
				subPathStr = mainXPath + "/" + subPathStr;
			processedSubPaths.push(subPathStr);
		}

		// We need to add our main XPath to the set of subPaths and generate a path
		// tree so we can find the XPath to the common parent of all the paths, just
		// in case the user specified a path that was outside of our main XPath.

		processedSubPaths.unshift(mainXPath);
		var commonParent = this.convertXPathsToPathTree(processedSubPaths);

		// The root node of the resulting path tree should contain the XPath
		// to the common parent. Make this the XPath we generate our initial
		// set of rows from so we can group the results of flattening the other
		// subPaths in predictable/expected manner.

		mainXPath = commonParent.path;
		subPaths = commonParent.subPaths;

		// If the XPath to the common parent we calculated isn't our main XPath
		// or any of the subPaths specified by the user, it is used purely for
		// grouping and joining the data we will flatten. We don't want to include
		// any of the columns for the rows created for the common parent XPath since
		// the user did not ask for it.

		suppressColumns = commonParent.xpath ? false : true;
	}

	rs = Spry.Data.XMLDataSet.getRecordSetFromXMLDoc(rawDataDoc, mainXPath, suppressColumns, this.entityEncodeStrings);

	if (!rs)
	{
		Spry.Debug.reportError("Spry.Data.XMLDataSet.loadDataIntoDataSet() failed to create dataSet '" + this.name + "'for '" + this.xpath + "' - " + this.url + "\n");
		return;
	}

	// Now that we have our base set of rows, flatten any additional subPaths
	// specified by the user.

	this.flattenSubPaths(rs, subPaths);

	this.doc = rs.xmlDoc;
	this.data = rs.data;
	this.dataHash = rs.dataHash;
	this.dataWasLoaded = (this.doc != null);
};

Spry.Data.XMLDataSet.prototype.xhRequestProcessor = function(xhRequest)
{
	// XMLDataSet uses the responseXML from the xhRequest

	var resp = xhRequest.responseXML;
	var manualParseRequired = false;

	if (xhRequest.status != 200)
	{
		if (xhRequest.status == 0)
		{
			// The page that is attempting to load data was probably loaded with
			// a file:// url. Mozilla based browsers will actually provide the complete DOM
			// tree for the data, but IE provides an empty document node so try to parse
			// the xml text manually to create a dom tree we can use.

			if (xhRequest.responseText && (!resp || !resp.firstChild))
				manualParseRequired = true;
		}
	}
	else if (!resp)
	{
		// The server said it sent us data, but for some reason we don't have
		// an XML DOM document. Some browsers won't auto-create an XML DOM
		// unless the server used a content-type of "text/xml" or "application/xml".
		// Try to manually parse the XML string, just in case the server
		// gave us an unexpected Content-Type.

		manualParseRequired = true;
	}

	if (manualParseRequired)
		resp = Spry.Utils.stringToXMLDoc(xhRequest.responseText);

	if (!resp  || !resp.firstChild || resp.firstChild.nodeName == "parsererror")
		return null;

	return resp;
};

Spry.Data.XMLDataSet.prototype.sessionExpiredChecker = function(req)
{
	if (req.xhRequest.responseText == 'session expired')
		return true;
	else
	{
		if (req.rawData)
		{
			var firstChild = req.rawData.documentElement.firstChild;
			if (firstChild && firstChild.nodeValue == "session expired")
				return true;
		}
	}
	return false;
};

//////////////////////////////////////////////////////////////////////
//
// Spry.Data.Region
//
//////////////////////////////////////////////////////////////////////

Spry.Data.Region = function(regionNode, name, isDetailRegion, data, dataSets, regionStates, regionStateMap, hasBehaviorAttributes)
{
	this.regionNode = regionNode;
	this.name = name;
	this.isDetailRegion = isDetailRegion;
	this.data = data;
	this.dataSets = dataSets;
	this.hasBehaviorAttributes = hasBehaviorAttributes;
	this.tokens = null;
	this.currentState = null;
	this.states = { ready: true };
	this.stateMap = {};

	Spry.Utils.setOptions(this.states, regionStates);
	Spry.Utils.setOptions(this.stateMap, regionStateMap);

	// Add the region as an observer to the dataSet!
	for (var i = 0; i < this.dataSets.length; i++)
	{
		var ds = this.dataSets[i];

		try
		{
			if (ds)
				ds.addObserver(this);
		}
		catch(e) { Spry.Debug.reportError("Failed to add '" + this.name + "' as a dataSet observer!\n"); }
	}
}; // End of Spry.Data.Region() constructor.

Spry.Data.Region.hiddenRegionClassName = "SpryHiddenRegion";
Spry.Data.Region.evenRowClassName = "even";
Spry.Data.Region.oddRowClassName = "odd";
Spry.Data.Region.notifiers = {};
Spry.Data.Region.evalScripts = true;

Spry.Data.Region.addObserver = function(regionID, observer)
{
	var n = Spry.Data.Region.notifiers[regionID];
	if (!n)
	{
		n = new Spry.Utils.Notifier();
		Spry.Data.Region.notifiers[regionID] = n;
	}
	n.addObserver(observer);
};

Spry.Data.Region.removeObserver = function(regionID, observer)
{
	var n = Spry.Data.Region.notifiers[regionID];
	if (n)
		n.removeObserver(observer);
};

Spry.Data.Region.notifyObservers = function(methodName, region, data)
{
	var n = Spry.Data.Region.notifiers[region.name];
	if (n)
	{
		var dataObj = {};
		if (data && typeof data == "object")
			dataObj = data;
		else
			dataObj.data = data;

		dataObj.region = region;
		dataObj.regionID = region.name;
		dataObj.regionNode = region.regionNode;

		n.notifyObservers(methodName, dataObj);
	}
};

Spry.Data.Region.RS_Error = 0x01;
Spry.Data.Region.RS_LoadingData = 0x02;
Spry.Data.Region.RS_PreUpdate = 0x04;
Spry.Data.Region.RS_PostUpdate = 0x08;

Spry.Data.Region.prototype.getState = function()
{
	return this.currentState;
};

Spry.Data.Region.prototype.mapState = function(stateName, newStateName)
{
	this.stateMap[stateName] = newStateName;
};

Spry.Data.Region.prototype.getMappedState = function(stateName)
{
	var mappedState = this.stateMap[stateName];
	return mappedState ? mappedState : stateName;
};

Spry.Data.Region.prototype.setState = function(stateName, suppressNotfications)
{
	var stateObj = { state: stateName, mappedState: this.getMappedState(stateName) };
	if (!suppressNotfications)
		Spry.Data.Region.notifyObservers("onPreStateChange", this, stateObj);

	this.currentState = stateObj.mappedState ? stateObj.mappedState : stateName;

	// If the region has content that is specific to this
	// state, regenerate the region so that its markup is updated.

	if (this.states[stateName])
	{
		var notificationData = { state: this.currentState };
		if (!suppressNotfications)
			Spry.Data.Region.notifyObservers("onPreUpdate", this, notificationData);

		// Make the region transform the xml data. The result is
		// a string that we need to parse and insert into the document.

		var str = this.transform();

		// Clear out any previous transformed content.
		// this.clearContent();

		if (Spry.Data.Region.debug)
			Spry.Debug.trace("<hr />Generated region markup for '" + this.name + "':<br /><br />" + Spry.Utils.encodeEntities(str));

		// Now insert the new transformed content into the document.
		Spry.Utils.setInnerHTML(this.regionNode, str, !Spry.Data.Region.evalScripts);

		// Now run through the content looking for attributes
		// that tell us what behaviors to attach to each element.
		if (this.hasBehaviorAttributes)
			this.attachBehaviors();

		if (!suppressNotfications)
			Spry.Data.Region.notifyObservers("onPostUpdate", this, notificationData);
	}

	if (!suppressNotfications)
		Spry.Data.Region.notifyObservers("onPostStateChange", this, stateObj);
};

Spry.Data.Region.prototype.getDataSets = function()
{
	return this.dataSets;
};

Spry.Data.Region.prototype.addDataSet = function(aDataSet)
{
	if (!aDataSet)
		return;

	if (!this.dataSets)
		this.dataSets = new Array;

	// Check to see if the data set is already in our list.

	for (var i = 0; i < this.dataSets.length; i++)
	{
		if (this.dataSets[i] == aDataSet)
			return; // It's already in our list!
	}

	this.dataSets.push(aDataSet);
	aDataSet.addObserver(this);
};

Spry.Data.Region.prototype.removeDataSet = function(aDataSet)
{
	if (!aDataSet || this.dataSets)
		return;

	for (var i = 0; i < this.dataSets.length; i++)
	{
		if (this.dataSets[i] == aDataSet)
		{
			this.dataSets.splice(i, 1);
			aDataSet.removeObserver(this);
			return;
		}
	}
};

Spry.Data.Region.prototype.onPreLoad = function(dataSet)
{
	if (this.currentState != "loading")
		this.setState("loading");
};

Spry.Data.Region.prototype.onLoadError = function(dataSet)
{
	if (this.currentState != "error")
		this.setState("error");
	Spry.Data.Region.notifyObservers("onError", this);
};

Spry.Data.Region.prototype.onSessionExpired = function(dataSet)
{
	if (this.currentState != "expired")
		this.setState("expired");
	Spry.Data.Region.notifyObservers("onExpired", this);
};

Spry.Data.Region.prototype.onCurrentRowChanged = function(dataSet, data)
{
	if (this.isDetailRegion)
		this.updateContent();
};

Spry.Data.Region.prototype.onPostSort = function(dataSet, data)
{
	this.updateContent();
};

Spry.Data.Region.prototype.onDataChanged = function(dataSet, data)
{
	this.updateContent();
};

Spry.Data.Region.enableBehaviorAttributes = true;
Spry.Data.Region.behaviorAttrs = {};

Spry.Data.Region.behaviorAttrs["spry:select"] =
{
	attach: function(rgn, node, value)
	{
		var selectGroupName = null;
		try { selectGroupName = node.attributes.getNamedItem("spry:selectgroup").value; } catch (e) {}
		if (!selectGroupName)
			selectGroupName = "default";

		Spry.Utils.addEventListener(node, "click", function(event) { Spry.Utils.SelectionManager.select(selectGroupName, node, value); }, false);

		if (node.attributes.getNamedItem("spry:selected"))
			Spry.Utils.SelectionManager.select(selectGroupName, node, value);
	}
};

Spry.Data.Region.behaviorAttrs["spry:hover"] =
{
	attach: function(rgn, node, value)
	{
		Spry.Utils.addEventListener(node, "mouseover", function(event){ Spry.Utils.addClassName(node, value); }, false);
		Spry.Utils.addEventListener(node, "mouseout", function(event){ Spry.Utils.removeClassName(node, value); }, false);
	}
};

Spry.Data.Region.setUpRowNumberForEvenOddAttr = function(node, attr, value, rowNumAttrName)
{
	// The format for the spry:even and spry:odd attributes are as follows:
	//
	// <div spry:even="dataSetName cssEvenClassName" spry:odd="dataSetName cssOddClassName">
	//
	// The dataSetName is optional, and if not specified, the first data set
	// listed for the region is used.
	//
	// cssEvenClassName and cssOddClassName are required and *must* be specified. They can be
	// any user defined CSS class name.

	if (!value)
	{
		Spry.Debug.showError("The " + attr + " attribute requires a CSS class name as its value!");
		node.attributes.removeNamedItem(attr);
		return;
	}

	var dsName = "";
	var valArr = value.split(/\s/);
	if (valArr.length > 1)
	{
		// Extract out the data set name and reset the attribute so
		// that it only contains the CSS class name to use.

		dsName = valArr[0];
		node.setAttribute(attr, valArr[1]);
	}

	// Tag the node with an attribute that will allow us to fetch the row
	// number used when it is written out during the re-generation process.

	node.setAttribute(rowNumAttrName, "{" + (dsName ? (dsName + "::") : "") + "ds_RowNumber}");
};

Spry.Data.Region.behaviorAttrs["spry:even"] =
{
	setup: function(node, value)
	{
		Spry.Data.Region.setUpRowNumberForEvenOddAttr(node, "spry:even", value, "spryevenrownumber");
	},

	attach: function(rgn, node, value)
	{
		if (value)
		{
			rowNumAttr = node.attributes.getNamedItem("spryevenrownumber");
			if (rowNumAttr && rowNumAttr.value)
			{
				var rowNum = parseInt(rowNumAttr.value);
				if (rowNum % 2)
					Spry.Utils.addClassName(node, value);
			}
		}
		node.removeAttribute("spry:even");
		node.removeAttribute("spryevenrownumber");
	}
};

Spry.Data.Region.behaviorAttrs["spry:odd"] =
{
	setup: function(node, value)
	{
		Spry.Data.Region.setUpRowNumberForEvenOddAttr(node, "spry:odd", value, "spryoddrownumber");
	},

	attach: function(rgn, node, value)
	{
		if (value)
		{
			rowNumAttr = node.attributes.getNamedItem("spryoddrownumber");
			if (rowNumAttr && rowNumAttr.value)
			{
				var rowNum = parseInt(rowNumAttr.value);
				if (rowNum % 2 == 0)
					Spry.Utils.addClassName(node, value);
			}
		}
		node.removeAttribute("spry:odd");
		node.removeAttribute("spryoddrownumber");
	}
};

Spry.Data.Region.setRowAttrClickHandler = function(node, dsName, rowAttr, funcName)
{
		if (dsName)
		{
			var ds = Spry.Data.getDataSetByName(dsName);
			if (ds)
			{
				rowIDAttr = node.attributes.getNamedItem(rowAttr);
				if (rowIDAttr)
				{
					var rowAttrVal = rowIDAttr.value;
					if (rowAttrVal)
						Spry.Utils.addEventListener(node, "click", function(event){ ds[funcName](rowAttrVal); }, false);
				}
			}
		}
};

Spry.Data.Region.behaviorAttrs["spry:setrow"] =
{
	setup: function(node, value)
	{
		if (!value)
		{
			Spry.Debug.reportError("The spry:setrow attribute requires a data set name as its value!");
			node.removeAttribute("spry:setrow");
			return;
		}

		// Tag the node with an attribute that will allow us to fetch the id of the
		// row used when it is written out during the re-generation process.

		node.setAttribute("spryrowid", "{" + value + "::ds_RowID}");
	},

	attach: function(rgn, node, value)
	{
		Spry.Data.Region.setRowAttrClickHandler(node, value, "spryrowid", "setCurrentRow");
		node.removeAttribute("spry:setrow");
		node.removeAttribute("spryrowid");
	}
};

Spry.Data.Region.behaviorAttrs["spry:setrownumber"] =
{
	setup: function(node, value)
	{
		if (!value)
		{
			Spry.Debug.reportError("The spry:setrownumber attribute requires a data set name as its value!");
			node.removeAttribute("spry:setrownumber");
			return;
		}

		// Tag the node with an attribute that will allow us to fetch the row number
		// of the row used when it is written out during the re-generation process.

		node.setAttribute("spryrownumber", "{" + value + "::ds_RowID}");
	},

	attach: function(rgn, node, value)
	{
		Spry.Data.Region.setRowAttrClickHandler(node, value, "spryrownumber", "setCurrentRowNumber");
		node.removeAttribute("spry:setrownumber");
		node.removeAttribute("spryrownumber");
	}
};

Spry.Data.Region.behaviorAttrs["spry:sort"] =
{
	attach: function(rgn, node, value)
	{
		if (!value)
			return;

		// The format of a spry:sort attribute is as follows:
		//
		// <div spry:sort="dataSetName column1Name column2Name ... sortOrderName">
		//
		// The dataSetName and sortOrderName are optional, but when specified, they
		// must appear in the order mentioned above. If the dataSetName is not specified,
		// the first data set listed for the region is used. If the sortOrderName is not
		// specified, the sort defaults to "toggle".
		//
		// The user *must* specify at least one column name.

		var ds = rgn.getDataSets()[0];
		var sortOrder = "toggle";

		var colArray = value.split(/\s/);
		if (colArray.length > 1)
		{
			// Check the first string in the attribute to see if a data set was
			// specified. If so, make sure we use it for the sort.

			var specifiedDS = Spry.Data.getDataSetByName(colArray[0]);
			if (specifiedDS)
			{
				ds = specifiedDS;
				colArray.shift();
			}

			// Check to see if the last string in the attribute is the name of
			// a sort order. If so, use that sort order during the sort.

			if (colArray.length > 1)
			{
				var str = colArray[colArray.length - 1];
				if (str == "ascending" || str == "descending" || str == "toggle")
				{
					sortOrder = str;
					colArray.pop();
				}
			}
		}

		// If we have a data set and some column names, add a non-destructive
		// onclick handler that will perform a toggle sort on the data set.

		if (ds && colArray.length > 0)
			Spry.Utils.addEventListener(node, "click", function(event){ ds.sort(colArray, sortOrder); }, false);

		node.removeAttribute("spry:sort");
	}
};

Spry.Data.Region.prototype.attachBehaviors = function()
{
	var rgn = this;
	Spry.Utils.getNodesByFunc(this.regionNode, function(node)
	{
		if (!node || node.nodeType != 1 /* Node.ELEMENT_NODE */)
			return false;
		try
		{
			var bAttrs = Spry.Data.Region.behaviorAttrs;
			for (var bAttrName in bAttrs)
			{
				var attr = node.attributes.getNamedItem(bAttrName);
				if (attr)
				{
					var behavior = bAttrs[bAttrName];
					if (behavior && behavior.attach)
						behavior.attach(rgn, node, attr.value);
				}
			}
		} catch(e) {}

		return false;
	});
};

Spry.Data.Region.prototype.updateContent = function()
{
	var allDataSetsReady = true;

	var dsArray = this.getDataSets();

	if (!dsArray || dsArray.length < 1)
	{
		Spry.Debug.reportError("updateContent(): Region '" + this.name + "' has no data set!\n");
		return;
	}

	for (var i = 0; i < dsArray.length; i++)
	{
		var ds = dsArray[i];

		if (ds)
		{
			if (ds.getLoadDataRequestIsPending())
				allDataSetsReady = false;
			else if (!ds.getDataWasLoaded())
			{
				// Kick off the loading of the data if it hasn't happened yet.
				ds.loadData();
				allDataSetsReady = false;
			}
		}
	}

	if (!allDataSetsReady)
	{
		Spry.Data.Region.notifyObservers("onLoadingData", this);

		// Just return, this method will get called again automatically
		// as each data set load completes!
		return;
	}

	this.setState("ready");
};

Spry.Data.Region.prototype.clearContent = function()
{
	this.regionNode.innerHTML = "";
};

Spry.Data.Region.processContentPI = function(inStr)
{
	var outStr = "";
	var regexp = /<!--\s*<\/?spry:content\s*[^>]*>\s*-->/mg;
	var searchStartIndex = 0;
	var processingContentTag = 0;

	while (inStr.length)
	{
		var results = regexp.exec(inStr);
		if (!results || !results[0])
		{
			outStr += inStr.substr(searchStartIndex, inStr.length - searchStartIndex);
			break;
		}

		if (!processingContentTag && results.index != searchStartIndex)
		{
			// We found a match but it's not at the start of the inStr.
			// Create a string token for everything that precedes the match.
			outStr += inStr.substr(searchStartIndex, results.index - searchStartIndex);
		}

		if (results[0].search(/<\//) != -1)
		{
			--processingContentTag;
			if (processingContentTag)
				Spry.Debug.reportError("Nested spry:content regions are not allowed!\n");
		}
		else
		{
			++processingContentTag;
			var dataRefStr = results[0].replace(/.*\bdataref="/, "");
			outStr += dataRefStr.replace(/".*$/, "");
		}

		searchStartIndex = regexp.lastIndex;
	}

	return outStr;
};

Spry.Data.Region.prototype.tokenizeData = function(dataStr)
{
	// If there is no data, there's nothing to do.
	if (!dataStr)
		return null;

	var rootToken = new Spry.Data.Region.Token(Spry.Data.Region.Token.LIST_TOKEN, null, null, null);
	var tokenStack = new Array;
	var parseStr = Spry.Data.Region.processContentPI(dataStr);

	tokenStack.push(rootToken);

	// Create a regular expression that will match one of the following:
	//
	//   <spry:repeat select="regionName" test="true">
	//   </spry:repeat>
	//   {valueReference}
	var regexp = /((<!--\s*){0,1}<\/{0,1}spry:[^>]+>(\s*-->){0,1})|((\{|%7[bB])[^\}\s%]+(\}|%7[dD]))/mg;
	var searchStartIndex = 0;

	while(parseStr.length)
	{
		var results = regexp.exec(parseStr);
		var token = null;

		if (!results || !results[0])
		{
			// If we get here, the rest of the parseStr should be
			// just a plain string. Create a token for it and then
			// break out of the list.
			var str = parseStr.substr(searchStartIndex, parseStr.length - searchStartIndex);
			token = new Spry.Data.Region.Token(Spry.Data.Region.Token.STRING_TOKEN, null, str, str);
			tokenStack[tokenStack.length - 1].addChild(token);
			break;
		}

		if (results.index != searchStartIndex)
		{
			// We found a match but it's not at the start of the parseStr.
			// Create a string token for everything that precedes the match.
			var str = parseStr.substr(searchStartIndex, results.index - searchStartIndex);
			token = new Spry.Data.Region.Token(Spry.Data.Region.Token.STRING_TOKEN, null, str, str);
			tokenStack[tokenStack.length - 1].addChild(token);
		}

		// We found a string that needs to be turned into a token. Create a token
		// for it and then update parseStr for the next iteration.
		if (results[0].search(/^({|%7[bB])/) != -1 /* results[0].charAt(0) == '{' */)
		{
			var valueName = results[0];
			var regionStr = results[0];

			// Strip off brace and url encode brace chars inside the valueName.

			valueName = valueName.replace(/^({|%7[bB])/, "");
			valueName = valueName.replace(/(}|%7[dD])$/, "");

			// Check to see if our value begins with the name of a data set.
			// For example: {dataSet:tokenValue}. If it is, we need to save
			// the data set name so we know which data set to use to get the
			// value for the token during the region transform.

			var dataSetName = null;
			var splitArray = valueName.split(/::/);

			if (splitArray.length > 1)
			{
				dataSetName = splitArray[0];
				valueName = splitArray[1];
			}

			// Convert any url encoded braces to regular brace chars.

			regionStr = regionStr.replace(/^%7[bB]/, "{");
			regionStr = regionStr.replace(/%7[dD]$/, "}");

			// Now create a token for the placeholder.

			token = new Spry.Data.Region.Token(Spry.Data.Region.Token.VALUE_TOKEN, dataSetName, valueName, new String(regionStr));
			tokenStack[tokenStack.length - 1].addChild(token);
		}
		else if (results[0].charAt(0) == '<')
		{
			// Extract out the name of the processing instruction.
			var piName = results[0].replace(/^(<!--\s*){0,1}<\/?/, "");
			piName = piName.replace(/>(\s*-->){0,1}|\s.*$/, "");

			if (results[0].search(/<\//) != -1 /* results[0].charAt(1) == '/' */)
			{
				// We found a processing instruction close tag. Pop the top of the
				// token stack!
				//
				// XXX: We need to make sure that the close tag name matches the one
				//      on the top of the token stack!
				if (tokenStack[tokenStack.length - 1].tokenType != Spry.Data.Region.Token.PROCESSING_INSTRUCTION_TOKEN)
				{
					Spry.Debug.reportError("Invalid processing instruction close tag: " + piName + " -- " + results[0] + "\n");
					return null;
				}

				tokenStack.pop();
			}
			else
			{
				// Create the processing instruction token, add it as a child of the token
				// at the top of the token stack, and then push it on the stack so that it
				// becomes the parent of any tokens between it and its close tag.

				var piDesc = Spry.Data.Region.PI.instructions[piName];

				if (piDesc)
				{
					var dataSet = null;

					var selectedDataSetName = "";
					if (results[0].search(/^.*\bselect=\"/) != -1)
					{
						selectedDataSetName = results[0].replace(/^.*\bselect=\"/, "");
						selectedDataSetName = selectedDataSetName.replace(/".*$/, "");

						if (selectedDataSetName)
						{
							dataSet = Spry.Data.getDataSetByName(selectedDataSetName);
							if (!dataSet)
							{
								Spry.Debug.reportError("Failed to retrieve data set (" + selectedDataSetName + ") for " + piName + "\n");
								selectedDataSetName = "";
							}
						}
					}

					// Check if the repeat has a test attribute.
					var jsExpr = null;
					if (results[0].search(/^.*\btest=\"/) != -1)
					{
						jsExpr = results[0].replace(/^.*\btest=\"/, "");
						jsExpr = jsExpr.replace(/".*$/, "");
						jsExpr = Spry.Utils.decodeEntities(jsExpr);
					}

					// Check if the instruction has a state name specified.
					var regionState = null;
					if (results[0].search(/^.*\bname=\"/) != -1)
					{
						regionState = results[0].replace(/^.*\bname=\"/, "");
						regionState = regionState.replace(/".*$/, "");
						regionState = Spry.Utils.decodeEntities(regionState);
					}

					var piData = new Spry.Data.Region.Token.PIData(piName, selectedDataSetName, jsExpr, regionState);

					token = new Spry.Data.Region.Token(Spry.Data.Region.Token.PROCESSING_INSTRUCTION_TOKEN, dataSet, piData, new String(results[0]));

					tokenStack[tokenStack.length - 1].addChild(token);
					tokenStack.push(token);
				}
				else
				{
					Spry.Debug.reportError("Unsupported region processing instruction: " + results[0] + "\n");
					return null;
				}
			}
		}
		else
		{
			Spry.Debug.reportError("Invalid region token: " + results[0] + "\n");
			return null;
		}

		searchStartIndex = regexp.lastIndex;
	}

	return rootToken;
};

Spry.Data.Region.prototype.callScriptFunction = function(funcName, processContext)
{
	var result = undefined;

	funcName = funcName.replace(/^\s*\{?\s*function::\s*|\s*\}?\s*$/g, "");
	var func = Spry.Utils.getObjectByName(funcName);
	if (func)
		result = func(this.name, function() { return processContext.getValueFromDataSet.apply(processContext, arguments); });

	return result;
};

Spry.Data.Region.prototype.evaluateExpression = function(exprStr, processContext)
{
	var result = undefined;

	try
	{
		if (exprStr.search(/^\s*function::/) != -1)
			result = this.callScriptFunction(exprStr, processContext);
		else
			result = Spry.Utils.eval(Spry.Data.Region.processDataRefString(processContext, exprStr, null, true));
	}
	catch(e)
	{
		Spry.Debug.trace("Caught exception in Spry.Data.Region.prototype.evaluateExpression() while evaluating: " + Spry.Utils.encodeEntities(exprStr) + "\n    Exception:" + e + "\n");
	}

	return result;
};

Spry.Data.Region.prototype.processTokenChildren = function(outputArr, token, processContext)
{
	var children = token.children;
	var len = children.length;

	for (var i = 0; i < len; i++)
		this.processTokens(outputArr, children[i], processContext);
};

Spry.Data.Region.prototype.processTokens = function(outputArr, token, processContext)
{
	var i = 0;

	switch(token.tokenType)
	{
		case Spry.Data.Region.Token.LIST_TOKEN:
			this.processTokenChildren(outputArr, token, processContext);
			break;
		case Spry.Data.Region.Token.STRING_TOKEN:
			outputArr.push(token.data);
			break;
		case Spry.Data.Region.Token.PROCESSING_INSTRUCTION_TOKEN:
			if (token.data.name == "spry:repeat")
			{
				var dataSet = null;

				if (token.dataSet)
					dataSet = token.dataSet;
				else
					dataSet = this.dataSets[0];

				if (dataSet)
				{
					var dsContext = processContext.getDataSetContext(dataSet);
					if (!dsContext)
					{
						Spry.Debug.reportError("processTokens() failed to get a data set context!\n");
						break;
					}

					dsContext.pushState();

					var dataSetRows = dsContext.getData();
					var numRows = dataSetRows.length;
					for (i = 0; i < numRows; i++)
					{
						dsContext.setRowIndex(i);
						var testVal = true;

						if (token.data.jsExpr)
							testVal = this.evaluateExpression(token.data.jsExpr, processContext);

						if (testVal)
							this.processTokenChildren(outputArr, token, processContext);
					}
					dsContext.popState();
				}
			}
			else if (token.data.name == "spry:if")
			{
				var testVal = true;

				if (token.data.jsExpr)
					testVal = this.evaluateExpression(token.data.jsExpr, processContext);

				if (testVal)
					this.processTokenChildren(outputArr, token, processContext);
			}
			else if (token.data.name == "spry:choose")
			{
				var defaultChild = null;
				var childToProcess = null;
				var testVal = false;
				var j = 0;

				// All of the children of the spry:choose token should be of the type spry:when or spry:default.
				// Run through all of the spry:when children and see if any of their test expressions return true.
				// If one does, then process its children tokens. If none of the test expressions return true,
				// process the spry:default token's children, if it exists.

				for (j = 0; j < token.children.length; j++)
				{
					var child = token.children[j];
					if (child.tokenType == Spry.Data.Region.Token.PROCESSING_INSTRUCTION_TOKEN)
					{
						if (child.data.name == "spry:when")
						{
							if (child.data.jsExpr)
							{
								testVal = this.evaluateExpression(child.data.jsExpr, processContext);

								if (testVal)
								{
									childToProcess = child;
									break;
								}
							}
						}
						else if (child.data.name == "spry:default")
							defaultChild = child;
					}
				}

				// If we didn't find a match, use the token for the default case.

				if (!childToProcess && defaultChild)
					childToProcess = defaultChild;

				if (childToProcess)
					this.processTokenChildren(outputArr, childToProcess, processContext);
			}
			else if (token.data.name == "spry:state")
			{
				var testVal = true;

				if (!token.data.regionState || token.data.regionState == this.currentState)
					this.processTokenChildren(outputArr, token, processContext);
			}
			else
			{
				Spry.Debug.reportError("processTokens(): Unknown processing instruction: " + token.data.name + "\n");
				return "";
			}
			break;
		case Spry.Data.Region.Token.VALUE_TOKEN:

			var dataSet = token.dataSet;
			var val = undefined;

			if (dataSet && dataSet == "function")
			{
				// This value token doesn't contain a data set data reference, it
				// contains a function call, so call it.

				val = this.callScriptFunction(token.data, processContext);
			}
			else
			{
				if (!dataSet && this.dataSets && this.dataSets.length > 0 && this.dataSets[0])
				{
					// No dataSet was specified by the token, so use whatever the first
					// data set specified in the region.
	
					dataSet = this.dataSets[0];
				}
				if (!dataSet)
				{
					Spry.Debug.reportError("processTokens(): Value reference has no data set specified: " + token.regionStr + "\n");
					return "";
				}
	
				val = processContext.getValueFromDataSet(dataSet, token.data);
			}

			if (typeof val != "undefined")
				outputArr.push(val + "");

			break;
		default:
			Spry.Debug.reportError("processTokens(): Invalid token type: " + token.regionStr + "\n");
			break;
	}
};

Spry.Data.Region.prototype.transform = function()
{
	if (this.data && !this.tokens)
		this.tokens = this.tokenizeData(this.data);

	if (!this.tokens)
		return "";

	processContext = new Spry.Data.Region.ProcessingContext(this);
	if (!processContext)
		return "";

	// Now call processTokens to transform our tokens into real data strings.
	// We use an array to gather the strings during processing as a performance
	// enhancement for IE to avoid n-square problems of adding to an existing
	// string. For example:
	//
	//     for (var i = 0; i < token.children.length; i++)
	//       outputStr += this.processTokens(token.children[i], processContext);
	//
	// Using an array with a final join reduced one of our test cases  from over
	// a minute to about 15 seconds.

	var outputArr = [ "" ];
	this.processTokens(outputArr, this.tokens, processContext);
	return outputArr.join("");
};

Spry.Data.Region.PI = {};
Spry.Data.Region.PI.instructions = {};

Spry.Data.Region.PI.buildOpenTagForValueAttr = function(ele, piName, attrName)
{
	if (!ele || !piName)
		return "";

	var jsExpr = "";

	try
	{
		var testAttr = ele.attributes.getNamedItem(piName);
		if (testAttr && testAttr.value)
			jsExpr = Spry.Utils.encodeEntities(testAttr.value);
	}
	catch (e) { jsExpr = ""; }

	if (!jsExpr)
	{
		Spry.Debug.reportError(piName + " attribute requires a JavaScript expression that returns true or false!\n");
		return "";
	}

	return "<" + Spry.Data.Region.PI.instructions[piName].tagName + " " + attrName +"=\"" + jsExpr + "\">";
};

Spry.Data.Region.PI.buildOpenTagForTest = function(ele, piName)
{
	return Spry.Data.Region.PI.buildOpenTagForValueAttr(ele, piName, "test");
};

Spry.Data.Region.PI.buildOpenTagForState = function(ele, piName)
{
	return Spry.Data.Region.PI.buildOpenTagForValueAttr(ele, piName, "name");
};

Spry.Data.Region.PI.buildOpenTagForRepeat = function(ele, piName)
{
	if (!ele || !piName)
		return "";

	var selectAttrStr = "";

	try
	{
		var selectAttr = ele.attributes.getNamedItem(piName);
		if (selectAttr && selectAttr.value)
		{
			selectAttrStr = selectAttr.value;
			selectAttrStr = selectAttrStr.replace(/\s/g, "");
		}
	}
	catch (e) { selectAttrStr = ""; }

	if (!selectAttrStr)
	{
		Spry.Debug.reportError(piName + " attribute requires a data set name!\n");
		return "";
	}

	var testAttrStr = "";

	try
	{
		var testAttr = ele.attributes.getNamedItem("spry:test");
		if (testAttr)
		{
			if (testAttr.value)
				testAttrStr = " test=\"" + Spry.Utils.encodeEntities(testAttr.value) + "\"";
			ele.attributes.removeNamedItem(testAttr.nodeName);
		}
	}
	catch (e) { testAttrStr = ""; }

	return "<" + Spry.Data.Region.PI.instructions[piName].tagName + " select=\"" + selectAttrStr + "\"" + testAttrStr + ">";
};

Spry.Data.Region.PI.buildOpenTagForContent = function(ele, piName)
{
	if (!ele || !piName)
		return "";

	var dataRefStr = "";

	try
	{
		var contentAttr = ele.attributes.getNamedItem(piName);
		if (contentAttr && contentAttr.value)
			dataRefStr = Spry.Utils.encodeEntities(contentAttr.value);
	}
	catch (e) { dataRefStr = ""; }

	if (!dataRefStr)
	{
		Spry.Debug.reportError(piName + " attribute requires a data reference!\n");
		return "";
	}

	return "<" + Spry.Data.Region.PI.instructions[piName].tagName + " dataref=\"" + dataRefStr + "\">";
};

Spry.Data.Region.PI.buildOpenTag = function(ele, piName)
{
	return "<" + Spry.Data.Region.PI.instructions[piName].tagName + ">";
};

Spry.Data.Region.PI.buildCloseTag = function(ele, piName)
{
	return "</" + Spry.Data.Region.PI.instructions[piName].tagName + ">";
};

Spry.Data.Region.PI.instructions["spry:state"] = { tagName: "spry:state", childrenOnly: false, getOpenTag: Spry.Data.Region.PI.buildOpenTagForState, getCloseTag: Spry.Data.Region.PI.buildCloseTag };
Spry.Data.Region.PI.instructions["spry:if"] = { tagName: "spry:if", childrenOnly: false, getOpenTag: Spry.Data.Region.PI.buildOpenTagForTest, getCloseTag: Spry.Data.Region.PI.buildCloseTag };
Spry.Data.Region.PI.instructions["spry:repeat"] = { tagName: "spry:repeat", childrenOnly: false, getOpenTag: Spry.Data.Region.PI.buildOpenTagForRepeat, getCloseTag: Spry.Data.Region.PI.buildCloseTag };
Spry.Data.Region.PI.instructions["spry:repeatchildren"] = { tagName: "spry:repeat", childrenOnly: true, getOpenTag: Spry.Data.Region.PI.buildOpenTagForRepeat, getCloseTag: Spry.Data.Region.PI.buildCloseTag };
Spry.Data.Region.PI.instructions["spry:choose"] = { tagName: "spry:choose", childrenOnly: true, getOpenTag: Spry.Data.Region.PI.buildOpenTag, getCloseTag: Spry.Data.Region.PI.buildCloseTag };
Spry.Data.Region.PI.instructions["spry:when"] = { tagName: "spry:when", childrenOnly: false, getOpenTag: Spry.Data.Region.PI.buildOpenTagForTest, getCloseTag: Spry.Data.Region.PI.buildCloseTag };
Spry.Data.Region.PI.instructions["spry:default"] = { tagName: "spry:default", childrenOnly: false, getOpenTag: Spry.Data.Region.PI.buildOpenTag, getCloseTag: Spry.Data.Region.PI.buildCloseTag };
Spry.Data.Region.PI.instructions["spry:content"] = { tagName: "spry:content", childrenOnly: true, getOpenTag: Spry.Data.Region.PI.buildOpenTagForContent, getCloseTag: Spry.Data.Region.PI.buildCloseTag };

Spry.Data.Region.PI.orderedInstructions = [ "spry:state", "spry:if", "spry:repeat", "spry:repeatchildren", "spry:choose", "spry:when", "spry:default", "spry:content" ];

Spry.Data.Region.getTokensFromStr = function(str)
{
	// XXX: This will need to be modified if we support
	// tokens that use javascript between the braces!
	if (!str)
		return null;
	return str.match(/{[^}]+}/g);
};

Spry.Data.Region.processDataRefString = function(processingContext, regionStr, dataSetsToUse, isJSExpr)
{
	if (!regionStr)
		return "";

	if (!processingContext && !dataSetsToUse)
		return regionStr;

	var resultStr = "";
	var re = new RegExp("\\{([^\\}:]+::)?[^\\}]+\\}", "g");
	var startSearchIndex = 0;

	while (startSearchIndex < regionStr.length)
	{
		var reArray = re.exec(regionStr);
		if (!reArray || !reArray[0])
		{
			resultStr += regionStr.substr(startSearchIndex, regionStr.length - startSearchIndex);
			return resultStr;
		}

		if (reArray.index != startSearchIndex)
			resultStr += regionStr.substr(startSearchIndex, reArray.index - startSearchIndex);

		var dsName = "";
		if (reArray[0].search(/^\{[^}:]+::/) != -1)
			dsName = reArray[0].replace(/^\{|::.*/g, "");

		var fieldName = reArray[0].replace(/^\{|.*::|\}/g, "");
		var row = null;

		var val = "";

		if (processingContext)
			val = processingContext.getValueFromDataSet(dsName, fieldName);
		else
		{
			var ds = dsName ? dataSetsToUse[dsName] : dataSetsToUse[0];
			if (ds)
				val = ds.getValue(fieldName);
		}

		if (typeof val != "undefined")
		{
			val += ""; // Make sure val is converted to a string.
			resultStr += isJSExpr ? Spry.Utils.escapeQuotesAndLineBreaks(val) : val;
		}

		if (startSearchIndex == re.lastIndex)
		{
			// On IE if there was a match near the end of the string, it sometimes
			// leaves re.lastIndex pointing to the value it had before the last time
			// we called re.exec. We check for this case to prevent an infinite loop!
			// We need to write out any text in regionStr that comes after the last
			// match.

			var leftOverIndex = reArray.index + reArray[0].length;
			if (leftOverIndex < regionStr.length)
				resultStr += regionStr.substr(leftOverIndex);

			break;
		}

		startSearchIndex = re.lastIndex;
	}

	return resultStr;
};

Spry.Data.Region.strToDataSetsArray = function(str, returnRegionNames)
{
	var dataSetsArr = new Array;
	var foundHash = {};

	if (!str)
		return dataSetsArr;

	str = str.replace(/\s+/g, " ");
	str = str.replace(/^\s|\s$/g, "");
	var arr = str.split(/ /);


	for (var i = 0; i < arr.length; i++)
	{
		if (arr[i] && !Spry.Data.Region.PI.instructions[arr[i]])
		{
			try {
				var dataSet = Spry.Data.getDataSetByName(arr[i]);

				if (!foundHash[arr[i]])
				{
					if (returnRegionNames)
						dataSetsArr.push(arr[i]);
					else
						dataSetsArr.push(dataSet);
					foundHash[arr[i]] = true;
				}
			}
			catch (e) { /* Spry.Debug.trace("Caught exception: " + e + "\n"); */ }
		}
	}

	return dataSetsArr;
};

Spry.Data.Region.DSContext = function(dataSet, processingContext)
{
	var m_dataSet = dataSet;
	var m_processingContext = processingContext;
	var m_curRowIndexArray = [ { rowIndex: -1 } ]; // -1 means return whatever the current row is inside the data set.
	var m_parent = null;
	var m_children = [];

	// Private Methods:

	var getInternalRowIndex = function() { return m_curRowIndexArray[m_curRowIndexArray.length - 1].rowIndex; };

	// Public Methods:
	this.resetAll = function() { m_curRowIndexArray = [ { rowIndex: m_dataSet.getCurrentRow() } ] };
	this.getDataSet = function() { return m_dataSet; };
	this.getNumRows = function(unfiltered)
	{
		var data = this.getCurrentState().data;
		return data ? data.length : m_dataSet.getRowCount(unfiltered);
	};
	this.getData = function()
	{
		var data = this.getCurrentState().data;
		return data ? data : m_dataSet.getData();
	};
	this.setData = function(data)
	{
		this.getCurrentState().data = data;
	};
	this.getValue = function(valueName, rowContext)
	{
		var result = "";
		var curState = this.getCurrentState();
		var ds = curState.nestedDS ? curState.nestedDS : this.getDataSet();
		if (ds)
			result = ds.getValue(valueName, rowContext);
		return result;
	};
	this.getCurrentRow = function()
	{
		if (m_curRowIndexArray.length < 2 || getInternalRowIndex() < 0)
			return m_dataSet.getCurrentRow();

		var data = this.getData();
		var curRowIndex = getInternalRowIndex();

		if (curRowIndex < 0 || curRowIndex > data.length)
		{
			Spry.Debug.reportError("Invalid index used in Spry.Data.Region.DSContext.getCurrentRow()!\n");
			return null;
		}

		return data[curRowIndex];
	};
	this.getRowIndex = function()
	{
		var curRowIndex = getInternalRowIndex();
		if (curRowIndex >= 0)
			return curRowIndex;

		return m_dataSet.getRowNumber(m_dataSet.getCurrentRow());
	};
	this.setRowIndex = function(rowIndex)
	{
		this.getCurrentState().rowIndex = rowIndex;

		var data = this.getData();
		var numChildren = m_children.length;
		for (var i = 0; i < numChildren; i++)
			m_children[i].syncDataWithParentRow(this, rowIndex, data);
	};
	this.syncDataWithParentRow = function(parentDSContext, rowIndex, parentData)
	{
		var row = parentData[rowIndex];
		if (row)
		{
			nestedDS = m_dataSet.getNestedDataSetForParentRow(row);
			if (nestedDS)
			{
				var currentState = this.getCurrentState();
				currentState.nestedDS = nestedDS;
				currentState.data = nestedDS.getData();
				currentState.rowIndex = nestedDS.getCurrentRowNumber();

				// getCurrentRowNumber() will return a -1 if the nestedDS has
				// no data in it. If the rowIndex is -1, we need to reset it back to
				// zero so the dsContext doesn't attempt to use the *real* current
				// row of the data set.

				currentState.rowIndex = currentState.rowIndex < 0 ? 0 : currentState.rowIndex;

				var numChildren = m_children.length;
				for (var i = 0; i < numChildren; i++)
					m_children[i].syncDataWithParentRow(this, currentState.rowIndex, currentState.data);
			}
		}
	};
	this.pushState = function()
	{
		var curState = this.getCurrentState();
		var newState = new Object;
		newState.rowIndex = curState.rowIndex;
		newState.data = curState.data;
		newState.nestedDS = curState.nestedDS;

		m_curRowIndexArray.push(newState);

		var numChildren = m_children.length;
		for (var i = 0; i < numChildren; i++)
			m_children[i].pushState();
	};
	this.popState = function()
	{
		if (m_curRowIndexArray.length < 2)
		{
			// Our array should always have at least one element in it!
			Spry.Debug.reportError("Stack underflow in Spry.Data.Region.DSContext.popState()!\n");
			return;
		}

		var numChildren = m_children.length;
		for (var i = 0; i < numChildren; i++)
			m_children[i].popState();

		m_curRowIndexArray.pop();
	};
	this.getCurrentState = function()
	{
		return m_curRowIndexArray[m_curRowIndexArray.length - 1];
	};
	this.addChild = function(childDSContext)
	{
		var numChildren = m_children.length;
		for (var i = 0; i < numChildren; i++)
		{
			if (m_children[i] == childDSContext)
				return;
		}
		m_children.push(childDSContext);
	};
};

Spry.Data.Region.ProcessingContext = function(region)
{
	this.region = region;
	this.dataSetContexts = [];

	if (region && region.dataSets)
	{
		// Run through each data set in the list and check to see if we need
		// to add its parent to the list of data sets we track.
		var dsArray = region.dataSets.slice(0);
		var dsArrayLen = dsArray.length;
		for (var i = 0; i < dsArrayLen; i++)
		{
			var ds = region.dataSets[i];
			while (ds && ds.getParentDataSet)
			{
				var doesExist = false;
				ds = ds.getParentDataSet();
				if (ds && this.indexOf(dsArray, ds) == -1)
					dsArray.push(ds);
			}
		}

		// Create a data set context for every data set in our list.

		for (i = 0; i < dsArray.length; i++)
			this.dataSetContexts.push(new Spry.Data.Region.DSContext(dsArray[i], this));

		// Now run through the list of data set contexts and wire up the parent/child
		// relationships so that notifications get dispatched as expected.

		var dsContexts = this.dataSetContexts;
		var numDSContexts = dsContexts.length;

		for (i = 0; i < numDSContexts; i++)
		{
			var dsc = dsContexts[i];
			var ds = dsc.getDataSet();
			if (ds.getParentDataSet)
			{
				var parentDS = ds.getParentDataSet();
				if (parentDS)
				{
					var pdsc = this.getDataSetContext(parentDS);
					if (pdsc) pdsc.addChild(dsc);
				}
			}
		}
	}
};

Spry.Data.Region.ProcessingContext.prototype.indexOf = function(arr, item)
{
	// Given an array, return the index of item in that array
	// or -1 if it doesn't exist.

	if (arr)
	{
		var arrLen = arr.length;
		for (var i = 0; i < arrLen; i++)
			if (arr[i] == item)
				return i;
	}
	return -1;
};

Spry.Data.Region.ProcessingContext.prototype.getDataSetContext = function(dataSet)
{
	if (!dataSet)
	{
		// We were called without a specified data set or
		// data set name. Assume the caller wants the first
		// data set in the processing context.

		if (this.dataSetContexts.length > 0)
			return this.dataSetContexts[0];
		return null;
	}

	if (typeof dataSet == 'string')
	{
		dataSet = Spry.Data.getDataSetByName(dataSet);
		if (!dataSet)
			return null;
	}

	for (var i = 0; i < this.dataSetContexts.length; i++)
	{
		var dsc = this.dataSetContexts[i];
		if (dsc.getDataSet() == dataSet)
			return dsc;
	}

	return null;
};

Spry.Data.Region.ProcessingContext.prototype.getValueFromDataSet = function()
{
	var dsName = "";
	var columnName = "";

	if (arguments.length > 1)
	{
		// The caller is passing in the data set name and the
		// name of the data reference separately.

		dsName = arguments[0];
		columnName = arguments[1];
	}
	else
	{
		// The caller is passing a single string which can be in one
		// of the following forms:
		//
		//    "columnName"
		//    "dsName::columnName"
		//    "{columnName}"
		//    "{dsName::columnName}"

		var dataRef = arguments[0].replace(/\s*{\s*|\s*}\s*/g, "");
		if (dataRef.search("::") != -1)
		{
			dsName = dataRef.replace(/::.*/, "");
			columnName = dataRef.replace(/.*::/, "");
		}
		else
			columnName = dataRef;
	}

	var result = "";
	var dsContext = this.getDataSetContext(dsName);
	if (dsContext)
		result = dsContext.getValue(columnName, dsContext.getCurrentRow());
	else
		Spry.Debug.reportError("getValueFromDataSet: Failed to get " + dsName + " context for the " + this.region.regionNode.id + " region.\n");

	return result;
};

// Define a short-hand name for developers.
Spry.Data.Region.ProcessingContext.prototype.$v = Spry.Data.Region.ProcessingContext.prototype.getValueFromDataSet;

Spry.Data.Region.ProcessingContext.prototype.getCurrentRowForDataSet = function(dataSet)
{
	var dsc = this.getDataSetContext(dataSet);
	if (dsc)
		return dsc.getCurrentRow();
	return null;
};

Spry.Data.Region.Token = function(tokenType, dataSet, data, regionStr)
{
	var self = this;
	this.tokenType = tokenType;
	this.dataSet = dataSet;
	this.data = data;
	this.regionStr = regionStr;
	this.parent = null;
	this.children = null;
};

Spry.Data.Region.Token.prototype.addChild = function(child)
{
	if (!child)
		return;

	if (!this.children)
		this.children = new Array;

	this.children.push(child);
	child.parent = this;
};

Spry.Data.Region.Token.LIST_TOKEN                   = 0;
Spry.Data.Region.Token.STRING_TOKEN                 = 1;
Spry.Data.Region.Token.PROCESSING_INSTRUCTION_TOKEN = 2;
Spry.Data.Region.Token.VALUE_TOKEN                  = 3;

Spry.Data.Region.Token.PIData = function(piName, data, jsExpr, regionState)
{
	var self = this;
	this.name = piName;
	this.data = data;
	this.jsExpr = jsExpr;
	this.regionState = regionState;
};

Spry.Utils.addLoadListener(function() { setTimeout(function() { if (Spry.Data.initRegionsOnLoad) Spry.Data.initRegions(); }, 0); });

// SpryHTMLDataSet.js - version 0.20 - Spry Pre-Release 1.6.1
//
// Copyright (c) 2006. Adobe Systems Incorporated.
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
//   * Redistributions of source code must retain the above copyright notice,
//     this list of conditions and the following disclaimer.
//   * Redistributions in binary form must reproduce the above copyright notice,
//     this list of conditions and the following disclaimer in the documentation
//     and/or other materials provided with the distribution.
//   * Neither the name of Adobe Systems Incorporated nor the names of its
//     contributors may be used to endorse or promote products derived from this
//     software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.

//////////////////////////////////////////////////////////////////////
//
// Spry.Data.HTMLDataSet
//
//////////////////////////////////////////////////////////////////////

Spry.Data.HTMLDataSet = function(dataSetURL, sourceElementID, dataSetOptions)
{
	this.sourceElementID = sourceElementID; // ID of the html element to be used as a data source
	this.sourceElement = null;  			      // The actual html element to be used as a data source

	this.sourceWasInitialized = false;
	this.usesExternalFile = (dataSetURL != null) ? true : false;
	
	this.firstRowAsHeaders = true;
	this.useColumnsAsRows = false;
	this.columnNames = null;
	this.hideDataSourceElement = true;
	
	this.rowSelector = null;
	this.dataSelector = null;

	this.tableModeEnabled = true;

	Spry.Data.HTTPSourceDataSet.call(this, dataSetURL, dataSetOptions);
};


Spry.Data.HTMLDataSet.prototype = new Spry.Data.HTTPSourceDataSet();
Spry.Data.HTMLDataSet.prototype.constructor = Spry.Data.HTMLDataSet;


Spry.Data.HTMLDataSet.prototype.getDataRefStrings = function() 
{
	var dep = [];
	if (this.url) 
		dep.push(this.url);
	if (typeof this.sourceElementID == "string") 
		dep.push(this.sourceElementID);
	
	return dep;
};

Spry.Data.HTMLDataSet.prototype.setDisplay = function(ele, display)
{
	if( ele )
		ele.style.display = display;
};

Spry.Data.HTMLDataSet.prototype.initDataSource = function(callLoadData)
{
	if (!this.loadDependentDataSets())
		return;
	if (!this.usesExternalFile)
	{
		this.setSourceElement();
		if (this.hideDataSourceElement)
			this.setDisplay(this.sourceElement, "none");
	}
	//this.sourceWasInitialized = true;
};


Spry.Data.HTMLDataSet.prototype.setSourceElement = function (externalDataElement)
{
   // externalDataElement is the container that holds the data imported from the external file.
	this.sourceElement = null;
	if (!this.sourceElementID) 
	{
	  if (externalDataElement)
  	  this.sourceElement = externalDataElement;
  	else
  	{
  	  this.hideDataSourceElement = false;
  	  this.sourceElement = document.body;
  	}
	  return; 
	}
	
	var sourceElementID = Spry.Data.Region.processDataRefString(null, this.sourceElementID, this.dataSetsForDataRefStrings);
	if (!this.usesExternalFile)
	   this.sourceElement = Spry.$(sourceElementID);
	else
    if (externalDataElement) 
    {
      var foundElement = false;
      // looking for the specified ID in the current element node
      var sources = Spry.Utils.getNodesByFunc(externalDataElement, function(node)
    	{
    	    if (foundElement) 
    	      return false;
    			if (node.nodeType != 1)
    				return false;
    			if (node.id && node.id.toLowerCase() == sourceElementID.toLowerCase())
    			{
    			  foundElement = true;
    			  return true;
    			}
      });
      this.sourceElement = sources[0];
    }
    
	if (!this.sourceElement) 
		Spry.Debug.reportError("Spry.Data.HTMLDataSet: '" + sourceElementID + "' is not a valid element ID");
};


Spry.Data.HTMLDataSet.prototype.getSourceElement = function() { return this.sourceElement; };
Spry.Data.HTMLDataSet.prototype.getSourceElementID = function() { return this.sourceElementID; };
Spry.Data.HTMLDataSet.prototype.setSourceElementID = function(sourceElementID)
{
	if (this.sourceElementID != sourceElementID)
	{
		this.sourceElementID = sourceElementID;
		this.recalculateDataSetDependencies();
		this.dataWasLoaded = false;
	}
};

Spry.Data.HTMLDataSet.prototype.getDataSelector = function() { return this.dataSelector; };
Spry.Data.HTMLDataSet.prototype.setDataSelector = function(dataSelector)
{ 
  if (this.dataSelector != dataSelector)
  {
     this.dataSelector = dataSelector;
  	 this.dataWasLoaded = false;
  }
};

Spry.Data.HTMLDataSet.prototype.getRowSelector = function() { return this.rowSelector; };
Spry.Data.HTMLDataSet.prototype.setRowSelector = function(rowSelector)
{ 
  if (this.rowSelector != rowSelector)
  {
     this.rowSelector = rowSelector;
  	 this.dataWasLoaded = false;
  }
};


Spry.Data.HTMLDataSet.prototype.loadDataIntoDataSet = function(rawDataDoc)
{
	var responseText = rawDataDoc;
	responseText = Spry.Data.HTMLDataSet.cleanupSource(responseText);

	var div = document.createElement("div");
	div.id = "htmlsource" + this.internalID;
	div.innerHTML = responseText;

	this.setSourceElement(div);
	if (this.sourceElement)
	{
		var parsedStructure = this.getDataFromSourceElement();
		if (parsedStructure) 
		{
			this.dataHash = parsedStructure.dataHash;
			this.data = parsedStructure.data;
		}		
	}
	this.dataWasLoaded = true;
	div = null;
};


Spry.Data.HTMLDataSet.prototype.loadDependentDataSets = function() 
{
	if (this.hasDataRefStrings)
	{
		var allDataSetsReady = true;

		for (var i = 0; i < this.dataSetsForDataRefStrings.length; i++)
		{
			var ds = this.dataSetsForDataRefStrings[i];
			if (ds.getLoadDataRequestIsPending())
				allDataSetsReady = false;
			else if (!ds.getDataWasLoaded())
			{
				// Kick off the load of this data set!
				ds.loadData();
				allDataSetsReady = false;
			}
		}

		// If our data sets aren't ready, just return. We'll
		// get called back to load our data when they are all
		// done.

		if (!allDataSetsReady)
			return false;
	}
	return true;
};


Spry.Data.HTMLDataSet.prototype.loadData = function()
{
	this.cancelLoadData();
	this.initDataSource();
	
	var self = this;
	if (!this.usesExternalFile) 
	{
		this.notifyObservers("onPreLoad");
		
		this.dataHash = new Object;
		this.data = new Array;
		this.dataWasLoaded = false;
		this.unfilteredData = null;
		this.curRowID = 0;
		
		this.pendingRequest = new Object;
		this.pendingRequest.timer = setTimeout(function()
		{
			self.pendingRequest = null;
			var parsedStructure = self.getDataFromSourceElement();
			if (parsedStructure) 
			{
				self.dataHash = parsedStructure.dataHash;
				self.data = parsedStructure.data;
			}
			self.dataWasLoaded = true;
			
			self.disableNotifications();
			self.filterAndSortData();
			self.enableNotifications();
			
			self.notifyObservers("onPostLoad");
			self.notifyObservers("onDataChanged");	
		}, 0); 
	}
	else 
	{
		var url = Spry.Data.Region.processDataRefString(null, this.url, this.dataSetsForDataRefStrings);

		var postData = this.requestInfo.postData;
		if (postData && (typeof postData) == "string") 
			postData = Spry.Data.Region.processDataRefString(null, postData, this.dataSetsForDataRefStrings);
		this.notifyObservers("onPreLoad");
		
	
		this.dataHash = new Object;
		this.data = new Array;
		this.dataWasLoaded = false;
		this.unfilteredData = null;
		this.curRowID = 0;

		var req = this.requestInfo.clone();
		req.url = url;
		req.postData = postData;
	
		this.pendingRequest = new Object;
		this.pendingRequest.data = Spry.Data.HTTPSourceDataSet.LoadManager.loadData(req, this, this.useCache);
	}
};


Spry.Data.HTMLDataSet.cleanupSource = function (source)
{
	// Cleans the content by replacing the src/href with spry_src 
	// This prevents browser to load the external resources.
  source = source.replace(/<(img|script|link|frame|iframe|input)([^>]+)>/gi, function(a,b,c) {
			//b=tag name,c=tag attributes
			return '<' + b + c.replace(/\b(src|href)\s*=/gi, function(a, b) {
				//b=attribute name
				return 'spry_'+ b + '=';
			}) + '>';
		});
	return source;
};


Spry.Data.HTMLDataSet.undoCleanupSource = function (source)
{
	// Undo cleanup. See the cleanupSource function
	source = source.replace(/<(img|script|link|frame|iframe|input)([^>]+)>/gi, function(a,b,c) {
			//b=tag name,c=tag attributes
			return '<' + b + c.replace(/\bspry_(src|href)\s*=/gi, function(a, b) {
				//b=attribute name
				return b + '=';
			}) + '>';
		});
	return source;
};


Spry.Data.HTMLDataSet.normalizeColumnName = function(colName) 
{
  // Removes the tags from column names values
  // Replaces spaces with underscore
	colName = colName.replace(/(?:^[\s\t]+|[\s\t]+$)/gi, "");
	colName = colName.replace(/<\/?([a-z]+)([^>]+)>/gi, "");
	colName = colName.replace(/[\s\t]+/gi, "_");
	return colName;
};


Spry.Data.HTMLDataSet.prototype.getDataFromSourceElement = function() 
{
	if (!this.sourceElement) 
    return null;

	var extractedData;
	var usesTable = (this.tableModeEnabled && this.sourceElement.nodeName.toLowerCase() == "table");
	if (usesTable)
		extractedData = this.getDataFromHTMLTable();
	else
		extractedData = this.getDataFromNestedStructure();

	if (!extractedData)
     return null;

	// Flip Columns / Rows
	if (this.useColumnsAsRows) 
	{
	   var flipedData = new Array;
	   // Get columns and put them as rows 
	   for (var rowIdx = 0; rowIdx < extractedData.length; rowIdx++)
	   {
	     var row = extractedData[rowIdx];
	     for (var cellIdx = 0; cellIdx < row.length; cellIdx++) 
	     {
	       if (!flipedData[cellIdx]) flipedData[cellIdx] = new Array;
	       flipedData[cellIdx][rowIdx]= row[cellIdx];
	     }
	   }
	   extractedData = flipedData;
	}

	// Build the data structure for the DataSet
	var parsedStructure = new Object();
	parsedStructure.dataHash = new Object;
	parsedStructure.data = new Array;
	
	if (extractedData.length == 0) 
	   return parsedStructure;
	   
	 
	// Get the column names
	// this.firstRowAsHeaders is used only if the source of data is a TABLE
	var columnNames = new Array;
	var firstRowOfData = extractedData[0];
	for (var cellIdx=0; cellIdx < firstRowOfData.length; cellIdx++)
	{
	  if (usesTable && this.firstRowAsHeaders) columnNames[cellIdx] = Spry.Data.HTMLDataSet.normalizeColumnName(firstRowOfData[cellIdx]);
	  else columnNames[cellIdx] = "column" + cellIdx;
	}
  
  // Check if column names are being overwritten using the optional columnNames parameter
  if (this.columnNames && this.columnNames.length) 
  {
    if (this.columnNames.length < columnNames.length) 
        Spry.Debug.reportError("Too few elements in the columnNames array. The columNames length must match the actual number of columns." );
    else
       for (var i=0; i < columnNames.length; i++) {
         if (this.columnNames[i]) columnNames[i] = this.columnNames[i];
    }
  }
  
  // Place the extracted data into a dataset kind of structure
	var nextID = 0;
	var firstDataRowIndex = (usesTable && this.firstRowAsHeaders) ? 1: 0;

	for (var rowIdx = firstDataRowIndex; rowIdx < extractedData.length; rowIdx++)
	{
		var row = extractedData[rowIdx];
		if (columnNames.length != row.length)
		{
			Spry.Debug.reportError("Unbalanced column names for row #" + (rowIdx+1) + ". Skipping row." );
			continue;
		}

		var rowObj = {};
		for (var cellIdx = 0; cellIdx < row.length; cellIdx++)
       rowObj[columnNames[cellIdx]] = row[cellIdx];

    rowObj['ds_RowID'] = nextID++;
    parsedStructure.dataHash[rowObj['ds_RowID']] = rowObj;
    parsedStructure.data.push(rowObj);
	}
	return parsedStructure;
};


Spry.Data.HTMLDataSet.getElementChildren = function(element)
{
	var children = [];
	var child = element.firstChild;
	while (child)
	{
		if (child.nodeType == 1)
			children.push(child);
		child = child.nextSibling;
	}
	return children;
};


// This method extracts data from a TABLE structure
// It knows how to handle both colspan and rowspan

Spry.Data.HTMLDataSet.prototype.getDataFromHTMLTable = function()
{
  var tHead = this.sourceElement.tHead;
  var tBody = this.sourceElement.tBodies[0];
  
  var rowsHead = [];
  var rowsBody = [];
  if (tHead) rowsHead = Spry.Data.HTMLDataSet.getElementChildren(tHead);
  if (tBody) rowsBody = Spry.Data.HTMLDataSet.getElementChildren(tBody);
  
  var extractedData = new Array;
  var rows = rowsHead.concat(rowsBody);
  if (this.rowSelector) rows = Spry.Data.HTMLDataSet.applySelector(rows, this.rowSelector);
  for (var rowIdx = 0; rowIdx < rows.length; rowIdx++)
  {
     var row = rows[rowIdx];
     
     var dataRow;
     if (extractedData[rowIdx]) dataRow = extractedData[rowIdx];
     else dataRow = new Array;
     
     var offset = 0;
     var cells = row.cells;
     if (this.dataSelector) cells = Spry.Data.HTMLDataSet.applySelector(cells, this.dataSelector);
     for (var cellIdx=0; cellIdx < cells.length; cellIdx++)
     {
       var cell = cells[cellIdx];
       var nextCellIndex = cellIdx + offset;
       
       // Find the next available position
       while (dataRow[nextCellIndex])
       {
          offset ++;
          nextCellIndex ++;
       }
       var cellValue = Spry.Data.HTMLDataSet.undoCleanupSource(cell.innerHTML);
       dataRow[nextCellIndex] = cellValue;
       
       // Handle collspan
       var colspan = cell.colSpan;
       if (colspan == 0) colspan = 1;
       var startOffset = offset;
       for (var offIdx = 1; offIdx < colspan; offIdx++)
       {
         offset ++;
         nextCellIndex = cellIdx + offset;
         dataRow[nextCellIndex] = cellValue;
       }
       
       // Handle rowspan
       var rowspan = cell.rowSpan;
       if (rowspan == 0) rowspan = 1;
       for (var rowOffIdx = 1; rowOffIdx < rowspan; rowOffIdx++)
       {
         nextRowIndex = rowIdx + rowOffIdx;
         var nextDataRow;
         if (extractedData[nextRowIndex]) nextDataRow = extractedData[nextRowIndex];
         else nextDataRow = new Array;
         
         var rowSpanCellOffset = startOffset;
         for (var offIdx = 0; offIdx < colspan; offIdx++)
         {
           nextCellIndex = cellIdx + rowSpanCellOffset;
           nextDataRow[nextCellIndex] = cellValue;
           rowSpanCellOffset ++;
         }
         extractedData[nextRowIndex] = nextDataRow;
       }
      }
     extractedData[rowIdx] = dataRow;
  }
  return extractedData;
};



// This method extracts data from any HTML structure
// It uses rowSelector and dataSelector in order to build a three level nested structure - 
// Either one: rowSelector or dataSelector can miss

Spry.Data.HTMLDataSet.prototype.getDataFromNestedStructure = function()
{
  var extractedData = new Array;
  
  if (this.sourceElementID && !this.rowSelector && !this.dataSelector) 
  {
     // The whole sourceElementID is a single row, single cell structure;
     extractedData[0] = [Spry.Data.HTMLDataSet.undoCleanupSource(this.sourceElement.innerHTML)];
     return extractedData;
  }
  
  var self = this;
  // Get the rows
  var rows = [];
  if (!this.rowSelector)
     // If no rowSelector, there will be only one row
     rows = [this.sourceElement];
  else
     rows = Spry.Utils.getNodesByFunc(this.sourceElement, function(node) { 
            return Spry.Data.HTMLDataSet.evalSelector(node, self.sourceElement, self.rowSelector); 
           }); 
           
  // Get the data columns
  for (var rowIdx = 0; rowIdx < rows.length; rowIdx++)
  {
    var row = rows[rowIdx];
    // Get the cells that actually hold the data for each row
    var cells = [];
    if (!this.dataSelector)
      // If no dataSelector, the whole row is extracted as one cell row.
      cells = [row];
    else
      cells = Spry.Utils.getNodesByFunc(row, function(node) { 
               return Spry.Data.HTMLDataSet.evalSelector(node, row, self.dataSelector); 
              });
              
    extractedData[rowIdx] = new Array;
    for (var cellIdx = 0; cellIdx < cells.length; cellIdx ++)
       extractedData[rowIdx][cellIdx] = Spry.Data.HTMLDataSet.undoCleanupSource(cells[cellIdx].innerHTML);
  }
  return extractedData;
};

// Applies a css selector on a collection and returns the resulting elements
Spry.Data.HTMLDataSet.applySelector = function(collection, selector, root)
{
   var newCollection = [];
   for (var idx = 0; idx < collection.length; idx++)
   {
     var node = collection[idx];
     if (Spry.Data.HTMLDataSet.evalSelector(node, root?root:node.parentNode, selector))
        newCollection.push(node);
   }
   return newCollection;
};

// Checks if a specified node matches the specified css selector
Spry.Data.HTMLDataSet.evalSelector = function (node, root, selector)
{
  if (node.nodeType != 1)
 		return false;
 	if (node == root)
 	  return false;
 	  
 	// Comma delimited selectors can be passed in
 	// The node is selected if it matches one of the selectors
 	// #myID1, div#myID2, #myID3
  var selectors = selector.split(",");
  for (var idx = 0; idx < selectors.length; idx ++)
  {
    var currentSelector = selectors[idx].replace(/^\s+/, "").replace(/\s+$/, "");
   	var tagName = null;
   	var className = null;
   	var id = null;
   	
   	// Accepted values for the selector:
   	// DIV.myClass | DIV | .myClass | *.myClass
   	// DIV#myID | #myID
   	// > DIV.myClass : > points to the direct descendents
   	
   	var selected = true;
   	if (currentSelector.substring(0,1) == ">") 
   	{
   	    // Looking for direct descendants only
   	    if (node.parentNode != root) 
   	      selected = false;
   	    else
   	      currentSelector = currentSelector.substring(1).replace(/^\s+/, "");
   	}
   	if (selected) 
   	{
     	tagName = currentSelector.toLowerCase();
     	if (currentSelector.indexOf(".") != -1)
     	{
     	  var parts = currentSelector.split(".");
     	  tagName = parts[0];
     	  className = parts[1];
     	}
     	else if (currentSelector.indexOf("#") != -1)
     	{
     	  var parts = currentSelector.split("#");
     	  tagName = parts[0];
     	  id = parts[1];
     	}
   	}
   	if (selected && tagName != '' && tagName != '*')
   	    if (node.nodeName.toLowerCase() != tagName) 
   	       selected = false;
   	if (selected && id && node.id != id)
   	    selected = false;
    	if (selected && className && node.className.search(new RegExp('\\b' + className + '\\b', 'i')) ==-1) 
   	    selected = false;
   	if (selected)
   	 return true;
  }
  return false;
};

// SpryPagedView.js - version 0.7 - Spry Pre-Release 1.6.1
//
// Copyright (c) 2006. Adobe Systems Incorporated.
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
//   * Redistributions of source code must retain the above copyright notice,
//     this list of conditions and the following disclaimer.
//   * Redistributions in binary form must reproduce the above copyright notice,
//     this list of conditions and the following disclaimer in the documentation
//     and/or other materials provided with the distribution.
//   * Neither the name of Adobe Systems Incorporated nor the names of its
//     contributors may be used to endorse or promote products derived from this
//     software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.

var Spry; if (!Spry) Spry = {}; if (!Spry.Data) Spry.Data = {};

Spry.Data.PagedView = function(ds, options)
{
	Spry.Data.DataSet.call(this);

	this.ds = ds;

	// The max number of rows in a given page.

	this.pageSize = 10;

	// The unfiltered row index of the first item on the
	// current page.

	this.pageOffset = 0;

	// Does the user want the last page of data to always
	// be a full page? Default allows the last page to be
	// less than a page full.

	this.forceFullPages = false;

	// The unfiltered row index of the first item on the
	// current page. This may differ from the value in
	// this.pageOffset when forceFullPages is true and
	// the last page is being viewed.

	this.pageFirstItemOffset = 0;

	// Does the user want to use zero-based page indexes?
	// Default is one-based.

	this.useZeroBasedIndexes = false;

	// Does the user want to force the current row of the
	// data set being paged to always be on the current page?

	this.setCurrentRowOnPageChange = false;

	Spry.Utils.setOptions(this, options);

	// Set the adjustmentValue *after* options are set.
	// adjustmentValue is used in calculations to make sure
	// we calculate correct values in either zero-based or
	// one-based page index mode.

	this.adjustmentValue = 1;
	if (!this.useZeroBasedIndexes)
		this.adjustmentValue = 0;

	// Init the pageStop after the options are set.
	this.pageStop = this.pageOffset + this.pageSize;

	// Set up a dependency on the data set that was
	// passed into our constructor.

	this.ds.addObserver(this);

	// Extract and pre-process any data in the data set
	// if it exists.

	this.preProcessData();

	// Set up an initial filter if necessary on this data set so that
	// any data that is loaded is paged immediately.

	if (this.pageSize > 0)
		this.filter(this.getFilterFunc());
};

Spry.Data.PagedView.prototype = new Spry.Data.DataSet();
Spry.Data.PagedView.prototype.constructor = Spry.Data.PagedView;

Spry.Data.PagedView.prototype.setCurrentRow = function(rowID)
{
	// Pass on any setCurrentRow calls to the data set we
	// depend on.

	if (this.ds)
		this.ds.setCurrentRow(rowID);
};

Spry.Data.PagedView.prototype.setCurrentRowNumber = function(rowNumber)
{
	// Pass on any setCurrentRowNumber calls to the data set we
	// depend on.

	if (this.ds)
		this.ds.setCurrentRowNumber(rowNumber);
};

Spry.Data.PagedView.prototype.sort = function(columnNames, sortOrder)
{
	// We try to discourage developers from sorting the "view"
	// for a data set instead of the "view itself, but since this
	// "view" is implemented as a data set, some still insist on
	// sorting the "view".

	if (!columnNames)
		return;

	// We need to calculate the sort order and the set of columnNames
	// we are going to use so we can pass it when we fire off our
	// onPreSort and onPostSort notifications.

	if (typeof columnNames == "string")
		columnNames = [ columnNames, "ds_RowID" ];
	else if (columnNames.length < 2 && columnNames[0] != "ds_RowID")
		columnNames.push("ds_RowID");

	if (!sortOrder)
		sortOrder = "toggle";

	if (sortOrder == "toggle")
	{
		if (this.lastSortColumns.length > 0 && this.lastSortColumns[0] == columnNames[0] && this.lastSortOrder == "ascending")
			sortOrder = "descending";
		else
			sortOrder = "ascending";
	}

	var nData = {
		oldSortColumns: this.lastSortColumns,
		oldSortOrder: this.lastSortOrder,
		newSortColumns: columnNames,
		newSortOrder: sortOrder
	};


	this.notifyObservers("onPreSort", nData);

	// Disable notifications so that when we call our inherited
	// sort function, no notifications get fired off. We want to
	// delay any onPostSort notification until *after* we get a chance
	// to update our pager columns and reset the view to the first
	// page.

	this.disableNotifications();

	Spry.Data.DataSet.prototype.sort.call(this, columnNames, sortOrder);
	this.updatePagerColumns();
	this.firstPage();

	this.enableNotifications();

	this.notifyObservers("onPostSort", nData);
};

Spry.Data.PagedView.prototype.loadData = function()
{
	// Pass on any loadData requests to the data set we
	// depend on. This data set will automatically be notified
	// when the data set we depend on is done loading and will
	// update our data at that point.

	if (!this.ds || this.ds.getLoadDataRequestIsPending())
		return;

	if (!this.ds.getDataWasLoaded())
	{
		this.ds.loadData();
		return;
	}

	Spry.Data.DataSet.prototype.loadData.call(this);
};

Spry.Data.PagedView.prototype.onDataChanged = function(notifier, data)
{
	// The data in the data set we depend on has changed.
	// We need to extract and pre-process its data.

	this.setPageOffset(0);
	this.preProcessData();
};

Spry.Data.PagedView.prototype.onCurrentRowChanged = function(notifier, data)
{
	// this.preProcessData();
	var self = this;
	setTimeout(function() { self.notifyObservers("onCurrentRowChanged", data); }, 0);
};

Spry.Data.PagedView.prototype.onPostSort = Spry.Data.PagedView.prototype.onDataChanged;

Spry.Data.PagedView.prototype.updatePagerColumns = function()
{
	var rows = this.getData(true);
	if (!rows || rows.length < 1)
		return;

	var numRows = rows.length;
	var pageSize = (this.pageSize > 0) ? this.pageSize : numRows;
	var firstItem = 1;
	var lastItem = firstItem + pageSize - 1;
	lastItem = (lastItem < firstItem) ? firstItem : (lastItem > numRows ? numRows : lastItem);

	var pageNum = 1;
	var pageCount = parseInt((numRows + pageSize - 1) / pageSize);
	var pageItemCount = Math.min(numRows, pageSize);

	for (var i = 0; i < numRows; i++)
	{
		itemIndex = i + 1;
		if (itemIndex > lastItem)
		{
			firstItem = itemIndex;
			lastItem = firstItem + this.pageSize - 1;
			lastItem = (lastItem > numRows) ? numRows : lastItem;
			pageItemCount = Math.min(lastItem - firstItem + 1, pageSize);
			++pageNum;
		}
		var row = rows[i];
		if (row)
		{
			row.ds_PageNumber = pageNum;
			row.ds_PageSize = this.pageSize;
			row.ds_PageItemRowNumber = i;
			row.ds_PageItemNumber = itemIndex;
			row.ds_PageFirstItemNumber = firstItem;
			row.ds_PageLastItemNumber = lastItem;
			row.ds_PageItemCount = pageItemCount;
			row.ds_PageCount = pageCount;
			row.ds_PageTotalItemCount = numRows;
		}
	}
};

Spry.Data.PagedView.prototype.preProcessData = function()
{
	if (!this.ds || !this.ds.getDataWasLoaded())
		return;

	this.notifyObservers("onPreLoad");

	this.unfilteredData = null;
	this.data = [];
	this.dataHash = {};
	var rows = this.ds.getData();

	if (rows)
	{
		// Make a copy of the rows in the data set we are
		// going to page. We need to do this because we are going to
		// add custom columns to the rows that the Pager manages.

		var numRows = rows.length;

		for (var i = 0; i < numRows; i++)
		{
			var row = rows[i];
			var newRow = new Object();
			Spry.Utils.setOptions(newRow, row);
			this.data.push(newRow);
			this.dataHash[newRow.ds_RowID] = newRow;
		}

		if (numRows > 0)
			this.curRowID = rows[0].ds_RowID;
		this.updatePagerColumns();
	}

	// this.notifyObservers("onPostLoad");

	this.loadData();
};

Spry.Data.PagedView.prototype.getFilterFunc = function()
{
	var self = this;
	return function(ds, row, rowNumber) {
		if (rowNumber < self.pageOffset || rowNumber >= self.pageStop)
			return null;
		return row;
	};
};

Spry.Data.PagedView.prototype.setPageOffset = function(offset)
{
	var numRows = this.getData(true).length;

	this.pageFirstItemOffset = (offset < 0) ? 0 : offset;

	if (this.forceFullPages && offset > (numRows - this.pageSize))
		offset = numRows - this.pageSize;

	if (offset < 0)
		offset = 0;

	this.pageOffset = offset;
	this.pageStop = offset + this.pageSize;
};

Spry.Data.PagedView.prototype.filterDataSet = function(offset)
{
	if (this.pageSize < 1)
		return;

	this.setPageOffset(offset);

	// We need to reset the Pager's current row to the first
	// item in the page so that any regions that use the built-in
	// pager data references get refreshed to the correct data values.

	var rows = this.getData(true);
	if (rows && rows.length && rows[this.pageFirstItemOffset])
		this.curRowID = rows[this.pageFirstItemOffset].ds_RowID;

	if (this.setCurrentRowOnPageChange)
		this.ds.setCurrentRow(this.curRowID);

	// Re-apply our non-destructive filter on the ds:
	this.filter(this.getFilterFunc());
};

Spry.Data.PagedView.prototype.getPageCount = function()
{
	return parseInt((this.getData(true).length + this.pageSize - 1) / this.pageSize);
};

Spry.Data.PagedView.prototype.getCurrentPage = function()
{
	return parseInt((((this.pageFirstItemOffset != this.pageOffset) ? this.pageFirstItemOffset : this.pageOffset) + this.pageSize) / this.pageSize) - this.adjustmentValue;
};

Spry.Data.PagedView.prototype.goToPage = function(pageNum)
{
	pageNum = parseInt(pageNum);

	var numPages = this.getPageCount();
	if ((pageNum + this.adjustmentValue) < 1 || (pageNum + this.adjustmentValue) > numPages)
		return;
	var newOffset = (pageNum - 1 + this.adjustmentValue) * this.pageSize;
	this.filterDataSet(newOffset);
};

Spry.Data.PagedView.prototype.goToPageContainingRowID = function(rowID)
{
	this.goToPageContainingRowNumber(this.getRowNumber(this.getRowByID(rowID), true));
};

Spry.Data.PagedView.prototype.goToPageContainingRowNumber = function(rowNumber)
{
	// rowNumber should be the zero based index of the row in the
	// unfiltered data set!

	this.goToPage(this.getPageForRowNumber(rowNumber));
};

Spry.Data.PagedView.prototype.goToPageContainingItemNumber = function(itemNumber)
{
	// Item number is the same as the unfiltered row number plus one, so just subract
	// one to get the row number we need.

	this.goToPageContainingRowNumber(itemNumber - 1);
};

Spry.Data.PagedView.prototype.firstPage = function()
{
	this.goToPage(1 - this.adjustmentValue);
};

Spry.Data.PagedView.prototype.lastPage = function()
{
	this.goToPage(this.getPageCount() - this.adjustmentValue);
};

Spry.Data.PagedView.prototype.previousPage = function()
{
	this.goToPage(this.getCurrentPage() - 1);
};

Spry.Data.PagedView.prototype.nextPage = function()
{
	this.goToPage(this.getCurrentPage() + 1);
};

Spry.Data.PagedView.prototype.getPageForRowID = function(rowID)
{
	return this.getPageForRowNumber(this.getRowNumber(this.getRowByID(rowID), true));
};

Spry.Data.PagedView.prototype.getPageForRowNumber = function(rowNumber)
{
	return parseInt(rowNumber / this.pageSize) + 1 - this.adjustmentValue;
};

Spry.Data.PagedView.prototype.getPageForItemNumber = function(itemNumber)
{
	return this.getPageForRowNumber(itemNumber - 1);
};

Spry.Data.PagedView.prototype.getPageSize = function()
{
	return this.pageSize;
};

Spry.Data.PagedView.prototype.setPageSize = function(pageSize)
{
	if (this.pageSize == pageSize)
		return;

	if (pageSize < 1)
	{
		// The caller is trying to shut off paging. Remove the filter
		// we installed on the data set.

		this.pageSize = 0;
		this.setPageOffset(0);
		this.updatePagerColumns();
		this.filter(null);
	}
	else if (this.pageSize < 1)
	{
		this.pageSize = pageSize;
		this.setPageOffset(0);
		this.updatePagerColumns();
		this.filterDataSet(this.pageOffset);
	}
	else
	{
		// The caller is adjusting the pageSize, so go to the page
		// that contains the current pageOffset.

		this.pageSize = pageSize;
		this.updatePagerColumns();
		this.goToPage(this.getPageForRowNumber(this.pageFirstItemOffset));
	}
};

Spry.Data.PagedView.prototype.getPagingInfo = function()
{
	return new Spry.Data.PagedView.PagingInfo(this);
};

Spry.Data.PagedView.PagingInfo = function(pagedView)
{
	Spry.Data.DataSet.call(this);
	this.pagedView = pagedView;
	pagedView.addObserver(this);
};

Spry.Data.PagedView.PagingInfo.prototype = new Spry.Data.DataSet();
Spry.Data.PagedView.PagingInfo.prototype.constructor = Spry.Data.PagedView.PagingInfo;

Spry.Data.PagedView.PagingInfo.prototype.onDataChanged = function(notifier, data)
{
	this.extractInfo();
};

Spry.Data.PagedView.PagingInfo.prototype.onPostSort = Spry.Data.PagedView.PagingInfo.prototype.onDataChanged;

Spry.Data.PagedView.PagingInfo.prototype.extractInfo = function()
{
	var pv = this.pagedView;
	if (!pv || !pv.getDataWasLoaded())
		return;

	this.notifyObservers("onPreLoad");

	this.unfilteredData = null;
	this.data = [];
	this.dataHash = {};
	var rows = pv.getData(true);

	if (rows)
	{
		var numRows = rows.length;
		var numPages = pv.getPageCount();
		var i = 0;
		var id = 0;

		while (i < numRows)
		{
			var row = rows[i];
			var newRow = new Object();
			newRow.ds_RowID = id++;
			this.data.push(newRow);
			this.dataHash[newRow.ds_RowID] = newRow;
			
			newRow.ds_PageNumber = row.ds_PageNumber;
			newRow.ds_PageSize = row.ds_PageSize;
			newRow.ds_PageCount = row.ds_PageCount;
			newRow.ds_PageFirstItemNumber = row.ds_PageFirstItemNumber;
			newRow.ds_PageLastItemNumber = row.ds_PageLastItemNumber;
			newRow.ds_PageItemCount = row.ds_PageItemCount;
			newRow.ds_PageTotalItemCount = row.ds_PageTotalItemCount;
			i += newRow.ds_PageSize;
		}
		
		if (numRows > 0)
		{
			var self = this;
			var func = function(notificationType, notifier, data) {
				if (notificationType != "onPostLoad")
					return;
				self.removeObserver(func);
				self.setCurrentRowNumber(pv.getCurrentPage() - (pv.useZeroBasedIndexes ? 0 : 1));
 			};
			this.addObserver(func);
		}
	}

	// this.notifyObservers("onPostLoad");

	this.loadData();
};

function handleEnter (field, event) {
                var keyCode = event.keyCode ? event.keyCode : event.which ? event.which : event.charCode;
                if (keyCode == 13) {
                        var i;
                        for (i = 0; i < field.form.elements.length; i++)
                                if (field == field.form.elements[i])     
                                        break;
                        i = (i + 1) % field.form.elements.length;
                        field.form.elements[i].focus();
                        return false;
                }
                else
                return true;     
        }


function FilterData()
{
        var tf = document.getElementById("filterTF");
			
        var regExpStr = tf.value;

        if (!document.getElementById("containsCB").checked){
                regExpStr = "^" + regExpStr;
                var is_checked=false;
        }
        else{
                var is_checked=true;
        }
		
		
		
        var s_item = 'LINK_NAME';
        

        var regExp = new RegExp(regExpStr, "i");

        var filterFunc = function(ds, row, rowNumber)
        {
                var str = row[s_item];
                if (str && str.search(regExp) != -1)
                        return row;
                return null;
        };
        items.filter(filterFunc);
}

function reset_l(){
        items.filter();
}

function reset_l1(){
        items1.filter();
}



function FilterData1()
{
	var tf = document.getElementById("filterTF1");
	if (!tf.value)
	{
		// If the text field is empty, remove any filter
		// that is set on the data set.

		items1.filter(null);
		return;
	}

	// Set a filter on the data set that matches any row
	// that begins with the string in the text field.

	var regExpStr = tf.value;
	
	if (!document.getElementById("containsCB1").checked)
		regExpStr = "^" + regExpStr;

	var regExp = new RegExp(regExpStr, "i");
	
	var filterFunc = function(ds, row, rowNumber)
	{
		var str = row["LINK_NAME"];
		if (str && str.search(regExp) != -1)
			return row;
		return null;
	};

	items1.filter(filterFunc);
}


