var fdjt_revision='exported';
var fdjt_buildhost='dev.beingmeta.com';
var fdjt_buildtime='Sat Apr 2 23:10:12 UTC 2011';
/* -*- Mode: Javascript; -*- */

/* Copyright (C) 2009-2011 beingmeta, inc.
   This file was created from several component files and is
    part of the FDJT web toolkit (www.fdjt.org)

   This program comes with absolutely NO WARRANTY, including implied
   warranties of merchantability or fitness for any particular
   purpose.

   The copyright notice of the individual files are all prefixed by
    a copyright notice of the form "Copyright (C) ...".

    Use, modification, and redistribution of this program is permitted
    under either the GNU General Public License (GPL) Version 2 (or
    any later version) or under the GNU Lesser General Public License
    (version 3 or later).

    These licenses may be found at www.gnu.org, particularly:
      http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
      http://www.gnu.org/licenses/lgpl-3.0-standalone.html

*/

var fdjt_versions=((fdjt_versions)||(new Array()));
fdjt_versions.decl=function(name,num){
    if ((!(fdjt_versions[name]))||(fdjt_versions[name]<num)) fdjt_versions[name]=num;};
/* -*- Mode: Javascript; -*- */

var fdjt_string_id="$Id$";
var fdjt_string_version=parseInt("$Revision$".slice(10,-1));
fdjt_versions.decl("fdjt/string",fdjt_string_version);
fdjt_versions.decl("fdjt",fdjt_string_version);

/* Copyright (C) 2009-2011 beingmeta, inc.
   This file is a part of the FDJT web toolkit (www.fdjt.org)
   This file provides extended Javascript utility functions
   of various kinds.

   This program comes with absolutely NO WARRANTY, including implied
   warranties of merchantability or fitness for any particular
   purpose.

   Use, modification, and redistribution of this program is permitted
   under either the GNU General Public License (GPL) Version 2 (or
   any later version) or under the GNU Lesser General Public License
   (version 3 or later).

   These licenses may be found at www.gnu.org, particularly:
   http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
   http://www.gnu.org/licenses/lgpl-3.0-standalone.html

*/

var fdjtString=
    (function(){
	function fdjtString(string){
	    var output="";
	    var cmd=string.indexOf('%'); var i=1;
	    while (cmd>=0) {
		if (cmd>0) output=output+string.slice(0,cmd);
		if (string[cmd+1]==='%') output=output+'%';
		else if (string[cmd+1]==='o') {
		    var arg=arguments[i++];
		    if (typeof arg === 'string')
			output=output+"'"+arg+"'";
		    else if (typeof arg === 'number')
			output=output+arg;
		    else output=output+stringify(arg);}
		else if (string[cmd+1]==='j') {
		    var arg=arguments[i++];
		    output=output+JSON.stringify(arg);}
		else if (arguments[i])
		    output=output+arguments[i++];
		else if (typeof arguments[i] === 'undefined') {
		    output=output+'?undef?'; i++;}
		else output=output+arguments[i++];
		string=string.slice(cmd+2);
		cmd=string.indexOf('%');}
	    output=output+string;
	    return output;
	}

	fdjtString.revid="$Id$";
	fdjtString.version=parseInt("$Revision$".slice(10,-1));

	fdjtString.nbsp="\u00A0";
	fdjtString.middot="\u00B7";
	fdjtString.emdash="\u2013";
	fdjtString.endash="\u2014";
	fdjtString.lsq="\u2018";
	fdjtString.rsq="\u2019";
	fdjtString.ldq="\u201C";
	fdjtString.rdq="\u201D";

	function stringify(arg){
	    if (typeof arg === 'undefined') return '?undef?';
	    else if (!(arg)) return arg;
	    else if (arg.tagName) {
		var output="<"+arg.tagName;
		if (arg.className)
		    output=output+"."+arg.className.replace(/\s+/g,'.');
		if (arg.id) output=output+"#"+arg.id;
		return output+">";}
	    else if (arg.nodeType)
		if (arg.nodeType===3)
		    return '<"'+arg.nodeValue+'">';
	    else return '<'+arg.nodeType+'>';
	    else if (arg.oid) return arg.oid;
	    else if (arg._fdjtid) return '#@'+arg._fdjtid;
	    else if ((arg.type)&&(arg.target)) 
		return "["+arg.type+"@"+stringify(arg.target)+"]";
	    else return arg;};

	var spacechars=" \n\r\t\f\x0b\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u200b\u2028\u2029\u202f\u205f\u3000\uf3ff";

	fdjtString.truncate=function(string,lim){
	    if (!(lim)) lim=42;
	    if (string.length<lim) return string;
	    else return string.slice(0,lim);}

	fdjtString.isEmpty=function(string){
	    if (typeof string === "string")  {
		var i=0; var lim=string.length;
		if (lim===0) return true;
		while (i<lim) {
		    if (spacechars.indexOf(string[i])>=0) i++;
		    else return false;}
		return true;}
	    else return false;}

	fdjtString.findSplit=function(string,split,escape){
	    var start=0;
	    var next;
	    while ((next=string.indexOf(split,start))>=0) 
		if ((escape) && (next>0) && (string[next-1]===escape))
		    start=next+1;
	    else return next;
	    return -1;};

	fdjtString.split=function(string,split,escape,mapfn){
	    if ((mapfn) || (escape)) {
		var results=[];
		var start=0; var next;
		while ((next=string.indexOf(split,start))>=0) 
		    if ((escape) && (next>0) && (string[next-1]===escape))
			start=next+1;
		else if ((mapfn) && (next>start)) {
		    results.push(mapfn(string.slice(start,next))); start=next+1;}
		else if (next>start) {
		    results.push(string.slice(start,next)); start=next+1;}
		else start=next+1;
		if (string.length>start)
		    if (mapfn) results.push(mapfn(string.slice(start)));
		else results.push(string.slice(start));
		return results;}
	    else return string.split(split);};

	fdjtString.semiSplit=function(string,escape,mapfn){
	    if ((mapfn) || (escape)) {
		var results=[];
		var start=0; var next;
		while ((next=string.indexOf(';',start))>=0) 
		    if ((escape) && (next>0) && (string[next-1]===escape))
			start=next+1;
		else if ((mapfn) && (next>start)) {
		    results.push(mapfn(string.slice(start,next))); start=next+1;}
		else if (next>start) {
		    results.push(string.slice(start,next)); start=next+1;}
		else start=next+1;
		if (string.length>start)
		    if (mapfn) results.push(mapfn(string.slice(start)));
		else results.push(string.slice(start));
		return results;}
	    else return string.split(';');};

	fdjtString.lineSplit=function(string,escapes,mapfn){
	    if ((mapfn) || (escape)) {
		var results=[];
		var start=0; var next;
		while ((next=string.indexOf('\n',start))>=0) 
		    if ((escape) && (next>0) && (string[next-1]===escape))
			start=next+1;
		else if ((mapfn) && (next>start)) {
		    results.push(mapfn(string.slice(start,next))); start=next+1;}
		else if (next>start) {
		    results.push(string.slice(start,next)); start=next+1;}
		else start=next+1;
		if (string.length>start)
		    if (mapfn) results.push(mapfn(string.slice(start)));
		else results.push(string.slice(start));
		return results;}
	    else return string.split('\n');};

	var spacechars=" \n\r\t\f\x0b\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u200b\u2028\u2029\u202f\u205f\u3000\uf3ff";
	
	function trim(string){
	    var start=0; var len=string.length; 
	    if (len<=0) return string;
	    while ((start<len)&&
		   (spacechars.indexOf(string.charAt(start))>-1))
		start++;
	    if (start===len) return "";
	    var end=len-1;
	    while ((end>start)&&(spacechars.indexOf(string.charAt(end))>-1))
		end--;
	    if ((start>0)||(end<len)) return string.slice(start,end+1);
	    else return string;}
	fdjtString.trim=trim;

	function stdspace(string){
	    var spacechars=" \n\r\t\f\x0b\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u200b\u2028\u2029\u202f\u205f\u3000\uf3ff";
	    string=string.replace(/\s+/," ");
	    var start=0; var len=string.length; 
	    if (len<=0) return string;
	    while ((start<len)&&
		   (spacechars.indexOf(string.charAt(start))>-1))
		start++;
	    if (start===len) return "";
	    var end=len-1;
	    while ((end>start)&&(spacechars.indexOf(string.charAt(end))>-1))
		end--;
	    if ((start>0)||(end<len)) return string.slice(start,end+1);
	    else return string;}
	fdjtString.stdspace=stdspace;

	function flatten(string){
	    return string.replace(/\s+/," ");}
	fdjtString.flatten=flatten;

	function oneline(string){
	    string=trim(string);
	    var flat=string.replace(/\s*[\f\n\r]+\s+/gm," //\u00B7 ").
		replace(/\s*[\f\n\r]+\s*/gm," // ");
	    var tight=flat.replace(/\s\s+/g,"");
	    return tight;}
	fdjtString.oneline=oneline;

	function stripMarkup(string){
	    return string.replace(/<[^>]*>/g,"");}
	fdjtString.stripMarkup=stripMarkup;

	function unEscape(string){
	    if (string.indexOf('\\')>=0)
		return string.replace(/\\(.)/g,"$1");
	    else return string;}
	fdjtString.unEscape=unEscape;

	function unEntify(string) {
	    return string.replace(/&#(\d+);/g,
				  function(whole,paren) {
				      return String.fromCharCode(+paren);});}
	fdjtString.unEntify=unEntify;

	function padNum(num,digits){
	    var ndigits=
		((num<10)?(1):(num<100)?(2):(num<1000)?(3):(num<10000)?(4):
		 (num<100000)?(5):(num<1000000)?(6):(num<1000000)?(7):
		 (num<100000000)?(8):(num<1000000000)?(9):(num<10000000000)?(10):(11));
	    var nzeroes=digits-ndigits;
	    switch (nzeroes) {
	    case 0: return ""+num;
	    case 1: return "0"+num;
	    case 2: return "00"+num;
	    case 3: return "000"+num;
	    case 4: return "0000"+num;
	    case 5: return "00000"+num;
	    case 6: return "000000"+num;
	    case 7: return "0000000"+num;
	    case 8: return "00000000"+num;
	    case 9: return "000000000"+num;
	    case 10: return "0000000000"+num;
	    default: return ""+num;}}
	fdjtString.padNum=padNum;

	/* Getting initials */

	function getInitials(string){
	  var words=string.split(/\W/); var initials="";
	  var i=0; var lim=words.length;
	  while (i<lim) {
	    var word=words[i++];
	    if (word.length)
	      initials=initials+word.slice(0,1);}
	  return initials;}
	fdjtString.getInitials=getInitials;

	/* More string functions */

	function hasPrefix(string,prefix){
	    return ((string.indexOf(prefix))===0);}
	fdjtString.hasPrefix=hasPrefix;

	function hasSuffix(string,suffix){
	    return ((string.lastIndexOf(suffix))===(string.length-suffix.length));}
	fdjtString.hasSuffix=hasSuffix;

	function commonPrefix(string1,string2,brk,foldcase){
	    var i=0; var last=0;
	    while ((i<string1.length) && (i<string2.length))
		if ((string1[i]===string2[i])||
		    ((foldcase)&&(string1[i].toLowerCase()===string2[i].toLowerCase())))
		    if (brk)
			if (brk===string1[i]) {last=i-1; i++;}
	    else i++;
	    else last=i++;
	    else break;
	    if (last>0) return string1.slice(0,last+1);
	    else return false;}
	fdjtString.commonPrefix=commonPrefix;

	function commonSuffix(string1,string2,brk,foldcase){
	    var i=string1.length, j=string2.length; var last=0;
	    while ((i>=0) && (j>=0))
		if ((string1[i]===string2[j])||
		    ((foldcase)&&(string1[i].toLowerCase()===string2[i].toLowerCase())))
		    if (brk)
			if (brk===string1[i]) {last=i+1; i--; j--;}
	    else {i--; j--;}
	    else {last=i; i--; j--;}
	    else break;
	    if (last>0) return string1.slice(last);
	    else return false;}
	fdjtString.commonSuffix=commonSuffix;

	function stripSuffix(string){
	    var start=string.search(/\.\w+$/);
	    if (start>0) return string.slice(0,start);
	    else return string;}
	fdjtString.stripSuffix=stripSuffix;

	function arrayContains(array,element){
	    if (array.indexOf)
		return (array.indexOf(element)>=0);
	    else {
		var i=0; var len=array.length;
		while (i<len)
		    if (array[i]===element) return true;
		else i++;
		return false;}}

	function prefixAdd(ptree,string,i) {
	    var strings=ptree.strings;
	    if (i===string.length) 
		if ((strings.indexOf) ?
		    (strings.indexOf(string)>=0) :
		    (arrayContains(strings,string)))
		    return false;
	    else {
		strings.push(string);
		return true;}
	    else if (ptree.splits) {
		var splitchar=string[i];
		var split=ptree[splitchar];
		if (!(split)) {
		    // Create a new split
		    split={};
		    split.strings=[];
		    // We don't really use this, but it might be handy for debugging
		    split.splitchar=splitchar;
		    ptree[splitchar]=split;
		    ptree.splits.push(split);}
		if (prefixAdd(split,string,i+1)) {
		    strings.push(string);
		    return true;}
		else return false;}
	    else if (ptree.strings.length<5)
		if ((strings.indexOf) ?
		    (strings.indexOf(string)>=0) :
		    (arrayContains(strings,string)))
		    return false;
	    else {
		strings.push(string);
		return true;}
	    else {
		// Subdivide
		ptree.splits=[];
		var strings=ptree.strings;
		var j=0; while (j<strings.length) prefixAdd(ptree,strings[j++],i);
		return prefixAdd(ptree,string,i);}}
	fdjtString.prefixAdd=prefixAdd;

	function prefixFind(ptree,prefix,i,plen){
	    if (!(plen)) plen=prefix.length;
	    if (i===plen)
		return ptree.strings;
	    else if (ptree.strings.length<=5) {
		var strings=ptree.strings;
		var results=[];
		var j=0; while (j<strings.length) {
		    var string=strings[j++];
		    if (hasPrefix(string,prefix)) results.push(string);}
		if (results.length) return results;
		else return false;}
	    else {
		var split=ptree[prefix[i]];
		if (split) return prefixFind(split,prefix,i+1,plen);
		else return false;}}
	fdjtString.prefixFind=prefixFind;

	return fdjtString;})();

/* Emacs local variables
   ;;;  Local variables: ***
   ;;;  compile-command: "make; if test -f ../makefile; then cd ..; make; fi" ***
   ;;;  End: ***
*/
/* -*- Mode: Javascript; -*- */

var fdjt_time_id="$Id$";
var fdjt_time_version=parseInt("$Revision$".slice(10,-1));
fdjt_versions.decl("fdjt/time",fdjt_time_version);
fdjt_versions.decl("fdjt",fdjt_time_version);

/* Copyright (C) 2009-2011 beingmeta, inc.
   This file is a part of the FDJT web toolkit (www.fdjt.org)
   This file provides extended Javascript utility functions
    of various kinds.

   This program comes with absolutely NO WARRANTY, including implied
   warranties of merchantability or fitness for any particular
   purpose.

    Use, modification, and redistribution of this program is permitted
    under either the GNU General Public License (GPL) Version 2 (or
    any later version) or under the GNU Lesser General Public License
    (version 3 or later).

    These licenses may be found at www.gnu.org, particularly:
      http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
      http://www.gnu.org/licenses/lgpl-3.0-standalone.html

*/

/* Time functions */

var fdjtTime=
    (function (){
	function fdjtTime() {
	    return (new Date()).getTime();}
	fdjtTime.revid="$Id$";
	fdjtTime.version=parseInt("$Revision$".slice(10,-1));

	var loaded=fdjtTime.loaded=(new Date()).getTime();
	fdjtTime.tick=function(){
	    return Math.floor((new Date()).getTime()/1000);};

	fdjtTime.dateString=function(tstamp){
	    return tstamp.toDateString();};
	function shortString(tstamp){
	    var now=new Date();
	    var diff=(now.getTime()-tstamp.getTime())/1000;
	    if (diff>(12*3600))
		return tstamp.toDateString();
	    else {
		var hours=tstamp.getHours();
		var minutes=tstamp.getMinutes();
		return tstamp.toDateString()+" ("+
		    ((hours<10)?"0":"")+hours+":"+
		    ((minutes<10)?"0":"");}}
	fdjtTime.shortString=shortString;
	fdjtTime.tick2shortstring=function(tick){
	    return shortString(new Date(tick*1000));};

	fdjtTime.tick2string=function(tick){
	    return (new Date(tick*1000)).toString();};
	fdjtTime.tick2date=function(tick){
	    return (new Date(tick*1000)).toDateString();};
	fdjtTime.tick2locale=function(tick){
	    return (new Date(tick*1000)).toLocaleString();};
	fdjtTime.tick2time=function(tick){
	    return (new Date(tick*1000)).toTimeString();};

	fdjtTime.secs2string=function(interval){
	    if (interval===1)
		return _("%1 second",interval);
	    else if (interval<10)
		return _("%1 seconds",interval);
	    else if (interval<60)
		return _("~%1 seconds",Math.round(interval/60));
	    else if (interval<120) {
		var minutes=Math.floor(interval/60);
		var seconds=Math.round(interval-(minutes*60));
		if (seconds===1)
		    return _("one minute, one second");
		else return _("one minute, %1 seconds",seconds);}
	    else if (interval<3600) {
		var minutes=Math.floor(interval/60);
		return _("~%1 minutes",minutes);}
	    else if (interval<(2*3600)) {
		var hours=Math.floor(interval/3600);
		var minutes=Math.round((interval-(hours*3600))/60);
		if (minutes===1)
		    return _("one hour and one minutes");
		else return _("one hour, %1 minutes",minutes);}
	    else if (interval<(24*3600)) {
		var hours=Math.floor(interval/3600);
		return _("~%1 hours",hours);}
	    else if (interval<(2*24*3600)) {
		var hours=Math.floor((interval-24*3600)/3600);
		if (hours===1)
		    return _("one day and one hour");
		else return _("one day, %1 hours",hours);}
	    else if (interval<(7*24*3600)) {
		var days=Math.floor(interval/(24*3600));
		return _("%1 days",days);}
	    else if (interval<(14*24*3600)) {
		var days=Math.floor((interval-(7*24*3600))/(24*3600));
		if (days===1)
		    return "one week and one day";
		else return _("one week and %1 days",days);}
	    else {
		var weeks=Math.floor(interval/(7*24*3600));
		var days=Math.round((interval-(days*7*24*3600))/(7*24*3600));
		return _("%1 weeks, %2 days",weeks,days);}};

	fdjtTime.secs2short=function(interval){
	    // This is designed for short intervals
	    if (interval<0.001)
		return Math.round(interval*1000000)+"us";
	    else if (interval<0.1)
		return Math.round(interval*1000)+"ms";
	    else if (interval<120)
		return (Math.round(interval*100)/100)+"s";
	    else {
		var min=Math.round(interval/60);
		var secs=Math.round(interval-min*6000)/100;
		return min+"m"+secs+"s";}};

	fdjtTime.runTimes=function(pname,start){
	    var point=start; var report="";
	    var i=2; while (i<arguments.length) {
		var phase=arguments[i++]; var time=arguments[i++];
		report=report+"; "+phase+": "+
		    ((time.getTime()-point.getTime())/1000)+"s";
		point=time;}
	    return pname+" "+((point.getTime()-start.getTime())/1000)+"s"+report;};

	fdjtTime.diffTime=function(time1,time2){
	    if (!(time2)) time2=new Date();
	    var diff=time1.getTime()-time2.getTime();
	    if (diff>0) return diff/1000; else return -(diff/1000);
	};

	fdjtTime.ET=function(arg){
	    if (!(arg)) arg=new Date();
	    return (arg.getTime()-loaded)/1000;};

	function timeslice(fcns,slice,space,done){
	    if (typeof slice !== 'number') slice=100;
	    if (typeof space !== 'number') space=100;
	    var i=0; var lim=fcns.length;
	    var slicefn=function(){
		var timelim=fdjtTime()+slice;
		var nextspace=false;
		while (i<lim) {
		    var fcn=fcns[i++];
		    if (!(fcn)) continue;
		    else if (typeof fcn === 'number') {
			nextspace=fcn; break;}
		    else fcn();
		    if (fdjtTime()>timelim) break;}
		if ((i<lim)&&((!(done))||(!(done()))))
		    setTimeout(slicefn,nextspace||space);};
	    return slicefn();}
	fdjtTime.timeslice=timeslice;

	function slowmap(fn,vec,done,slice){
	    var i=0; var lim=vec.length;
	    if (!(slice)) slice=100;
	    var stepfn=function(){
		var stopat=fdjtTime()+slice;
		while ((i<lim)||(fdjtTime()<stopat)) fn(vec[i++]);
		if (i<lim) setTimeout(stepfn,slice);
		else if (done) done();};
	    setTimeout(stepfn,slice);}
	fdjtTime.slowmap=slowmap;

	return fdjtTime;})();

var fdjtET=fdjtTime.ET;

/* Emacs local variables
;;;  Local variables: ***
;;;  compile-command: "make; if test -f ../makefile; then cd ..; make; fi" ***
;;;  End: ***
*/
/* -*- Mode: Javascript; -*- */

var fdjt_dom_id="$Id$";
var fdjt_dom_version=parseInt("$Revision$".slice(10,-1));
fdjt_versions.decl("fdjt/dom",fdjt_dom_version);
fdjt_versions.decl("fdjt",fdjt_dom_version);

/* Copyright (C) 2009-2011 beingmeta, inc.
   This file is a part of the FDJT web toolkit (www.fdjt.org)
   This file provides extended Javascript utility functions
   of various kinds.

   This program comes with absolutely NO WARRANTY, including implied
   warranties of merchantability or fitness for any particular
   purpose.

   Use, modification, and redistribution of this program is permitted
   under either the GNU General Public License (GPL) Version 2 (or
   any later version) or under the GNU Lesser General Public License
   (version 3 or later).

   These licenses may be found at www.gnu.org, particularly:
   http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
   http://www.gnu.org/licenses/lgpl-3.0-standalone.html

*/

var fdjtDOM=
  (function(){
    var usenative=true;

    function fdjtDOM(spec){
      var node;
      if (spec.nodeType) node=spec;
      else if (typeof spec==='string')
	if (spec[0]==='<') {
	  var container=document.createElement("DIV");
	  container.innerHTML=spec;
	  var children=container.childNodes;
	  var i=0; var len=children.length;
	  while (i<len)
	    if (children[i].nodeType===1) return children[i];
	    else i++;
	  return false;}
	else {
	  var elts=spec.match(css_selector_regex);
	  var classname=false;
	  node=document.createElement(elts[0]);
	  var i=1; var len=elts.length;
	  while (i<len) {
	    var sel=elts[i++];
	    if (sel[0]==='#') node.id=sel.slice(1);
	    else if (sel[0]==='.')
	      if (classname) classname=classname+" "+sel.slice(1);
	      else classname=sel.slice(1);
	    else if (sel[0]==='[') {
	      var eqpos=sel.indexOf('=');
	      if (eqpos<0) {
		node.setAttribute(
				  sel.slice(1,sel.length-1),
				  sel.slice(1,sel.length-1));}
	      else {
		node.setAttribute(
				  sel.slice(1,eqpos),
				  sel.slice(eqpos+1,sel.length-1));}}
	    else {}}
	  if (classname) node.className=classname;}
      else {
	node=document.createElement(spec.tagName||"span");
	for (attrib in spec) {
	  if (attrib==="tagName") continue;
	  else node.setAttribute(attrib,spec[attrib]);}}
      domappend(node,arguments,1);
      return node;}

    fdjtDOM.revid="$Id$";
    fdjtDOM.version=parseInt("$Revision$".slice(10,-1));
    fdjtDOM.useNative=function(flag) {
      if (typeof flag === 'undefined') return usenative;
      else usenative=flag;};
	
    fdjtDOM.clone=function(node){
      return node.cloneNode(true);}

    function domappend(node,content,i) {
      if (content.nodeType)
	node.appendChild(content);
      else if (typeof content === 'string')
	node.appendChild(document.createTextNode(content));
      else if (content.toDOM)
	domappend(node,content.toDOM());
      else if (content.toHTML)
	domappend(node,content.toHTML());
      else if (content.length) {
	if (typeof i === 'undefined') i=0;
	var len=content.length;
	while (i<len) {
	  var elt=content[i++];
	  if (!(elt)) {}
	  else if (typeof elt === 'string')
	    node.appendChild(document.createTextNode(elt));
	  else if (elt.nodeType)
	    node.appendChild(elt);
	  else if (elt.length)
	    domappend(node,elt,0);
	  else if (elt.toDOM)
	    domappend(node,elt.toDOM());
	  else if (elt.toHTML)
	    domappend(node,elt.toHTML());
	  else if (elt.toString)
	    node.appendChild(document.createTextNode(elt.toString()));
	  else node.appendChild(document.createTextNode(""+elt));}}
      else node.appendChild(document.createTextNode(""+content));}
    function dominsert(before,content,i) {
      var node=before.parentNode;
      if (content.nodeType)
	node.insertBefore(content,before);
      else if (typeof content === 'string')
	node.insertBefore(content,before);
      else if (content.toDOM)
	dominsert(before,content.toDOM());
      else if (content.toHTML)
	dominsert(before,content.toHTML());
      else if (content.length) {
	if (typeof i === 'undefined') i=0;
	var j=content.length-1;
	while (j>=i) {
	  var elt=content[j--];
	  if (!(elt)) {}
	  else if (typeof elt === 'string')
	    node.insertBefore(document.createTextNode(elt),before);
	  else if (elt.nodeType)
	    node.insertBefore(elt,before);
	  else if (elt.length)
	    dominsert(before,elt,0);
	  else if (elt.toDOM)
	    dominsert(before,elt.toDOM());
	  else if (elt.toHTML)
	    dominsert(before,elt.toHTML());
	  else if (elt.toString)
	    node.insertBefore(document.createTextNode(elt.toString()),before);
	  else node.insertBefore(document.createTextNode(""+elt),before);}}
      else node.insertBefore(document.createTextNode(""+elt),before);}

    function toArray(arg) {
      var result=new Array(arg.length);
      var i=0; var lim=arg.length;
      while (i<lim) {result[i]=arg[i]; i++;}
      return result;}
    fdjtDOM.toArray=toArray;
    function extendArray(result,arg) {
      var i=0; var lim=arg.length;
      while (i<lim) {result.push(arg[i]); i++;}
      return result;}
    function TOA(arg,start) {
      if (arg instanceof Array) {
	if (start) return arg.slice(start);
	else return arg;}
      start=start||0;
      var i=0; var lim=arg.length-start;
      var result=new Array(lim);
      while (i<lim) {result[i]=arg[i+start]; i++;}
      return result;}
    fdjtDOM.Array=TOA;

    /* Utility patterns and functions */

    function parsePX(arg,dflt){
      if (typeof dflt === 'undefined') dflt=0;
      if (arg===0) return 0;
      else if (!(arg)) return dflt;
      else if (arg==="none") return dflt;
      else if (arg==="auto") return dflt;
      else if (typeof arg === 'number') return arg;
      else if (typeof arg === 'string') {
	var len=arg.length; var num=false;
	if ((len>2)&&(arg[len-1]==='x')&&(arg[len-2]==='p'))
	  num=parseInt(arg.slice(0,-2));
	else num=parseInt(arg);
	if (num===0) return 0;
	else if (isNaN(num)) return dflt;
	else if (typeof num === 'number') return num;
	else return dflt;}
      else return false;}
    fdjtDOM.parsePX=parsePX;

    var css_selector_regex=/((^|[.#])\w+)|(\[\w+=\w+\])/g;

    var whitespace_pat=/(\s)+/;
    var trimspace_pat=/^(\s)+|(\s)+$/;
    var classpats={};
    function classPat(name){
      var rx=new RegExp("\\b"+name+"\\b","g");
      classpats[name]=rx;
      return rx;};

    function string_trim(string){
      var start=string.search(/\S/); var end=string.search(/\s+$/g);
      if ((start===0) && (end<0)) return string;
      else return string.slice(start,end);}

    function nodeString(node){
      if (node.nodeType===3) 
	return "<'"+node.value+"'>";
      else if (node.nodeType===1) {
	var output="<"+node.tagName;
	if (node.id) output=output+"#"+node.id;
	if (node.tagName==='input') {
	  output+"[type="+node.type+"]";
	  output+"[name="+node.name+"]";}
	else if (node.tagName==='textarea')
	  output+"[name="+node.name+"]";
	else if (node.tagName==='img') {
	  if (node.alt) output=output+"[alt="+node.alt+"]";
	  else if (node.src) output=output+"[src="+node.src+"]";}
	else {}
	if (node.className)
	  output=output+"."+node.className.replace(/\s+/g,'.');
	return output+">";}
      else return node.toString();}
    fdjtDOM.nodeString=nodeString;
	
    /* Simple class/attrib manipulation functions */

    function hasClass(elt,classname,attrib){
      var classinfo=((attrib) ? (elt.getAttribute(attrib)||"") : (elt.className));
      if (!(classinfo)) return false;
      else if (classname===true) return true;
      else if (classinfo===classname) return true;
      else if (typeof classname === 'string')
	if (classinfo.indexOf(' ')<0) return false;
	else classname=classpats[classname]||classPat(classname);
      else {}
      if (classinfo.search(classname)>=0) return true;
      else return false;}
    fdjtDOM.hasClass=hasClass;

    function addClass(elt,classname,attrib){
      if (typeof elt === 'string') elt=document.getElementById(elt);
      else if (elt instanceof Array) { // (elt instanceof NodeList)
	var elts=((elt instanceof Array)?(elt):(toArray(elt)));
	var i=0; var lim=elts.length;
	while (i<lim) addClass(elts[i++],classname,attrib||false);
	return;}
      else if ((NodeList)&&(elt instanceof NodeList))
	return addClass(TOA(elt),classname,attrib);
      var classinfo=
	(((attrib) ? (elt.getAttribute(attrib)||"") :(elt.className))||null);
      if (!(classinfo)) {
	elt.className=classname; return true;}
      var class_regex=classpats[classname]||classPat(classname);
      var newinfo=classinfo;
      if (classinfo===classname) return false;
      else if (classinfo.search(class_regex)>=0) return false;
      else newinfo=classname+" "+classinfo;
      if (attrib) {
	elt.setAttribute(attrib,newinfo);
	// This sometimes trigger a CSS update that doesn't happen otherwise
	elt.className=elt.className;}
      else elt.className=newinfo;
      return true;}
    fdjtDOM.addClass=addClass;
    fdjtDOM.aC=addClass;

    fdjtDOM.classAdder=function(elt,classname){
      return function() {
	if (elt) addClass(elt,classname);}};

    function dropClass(elt,classname,attrib){
      if (typeof elt === 'string') elt=document.getElementById(elt);
      else if (elt instanceof Array) {
	var elts=((elt instanceof Array)?(elt):(toArray(elt)));
	var i=0; var lim=elts.length;
	while (i<lim) dropClass(elts[i++],classname,attrib||false);
	return;}
      else if ((NodeList)&&(elt instanceof NodeList))
	return dropClass(TOA(elt),classname,attrib);
      var classinfo=
	(((attrib) ? (elt.getAttribute(attrib)||"") :(elt.className))||null);
      if (!(classinfo)) return false;
      var class_regex=
	((typeof classname === 'string')?
	 (classpats[classname]||classPat(classname)):
	 classname);
      var newinfo=classinfo;
      if (classinfo===classname) 
	newinfo=null;
      else if (classinfo.search(class_regex)>=0) 
	newinfo=classinfo.replace(class_regex,"");
      else return false;
      if (newinfo)
	newinfo=newinfo.
	  replace(whitespace_pat," ").
	  replace(trimspace_pat,"");
      if (attrib)
	if (newinfo) {
	  elt.setAttribute(attrib,newinfo);
	  elt.className=elt.className;}
	else if (!(keep)) {
	  elt.removeAttribute(attrib);
	  elt.className=elt.className;}
	else {}
      else elt.className=newinfo;
      return true;}
    fdjtDOM.dropClass=dropClass;
    fdjtDOM.dC=dropClass;

    fdjtDOM.classDropper=function(elt,classname){
      return function() {
	if (elt) dropClass(elt,classname);}};

    function swapClass(elt,drop,add,attrib) {
      dropClass(elt,drop,attrib); addClass(elt,add,attrib);}
    fdjtDOM.swapClass=swapClass;

    function toggleClass(elt,classname,attrib){
      if (typeof elt === 'string') elt=document.getElementById(elt);
      else if (elt instanceof Array) { // (elt instanceof NodeList)
	var elts=((elt instanceof Array)?(elt):(toArray(elt)));
	var i=0; var lim=elts.length;
	while (i<lim) toggleClass(elts[i++],classname,attrib||false);
	return;}
      else if ((NodeList)&&(elt instanceof NodeList))
	return toggleClass(TOA(elt),classname,attrib);
      var classinfo=
	(((attrib) ? (elt.getAttribute(attrib)||"") :
	  (elt.className))||null);
      if (!(classinfo)) {
	if (attrib) elt.setAttribute(attrib,classname);
	else elt.className=classname;
	return true;}
      var class_regex=
	((typeof classname === 'string')?
	 (classpats[classname]||classPat(classname)):
	 classname);
      var newinfo=classinfo;
      if (classinfo===classname) 
	newinfo=null;
      else if (classinfo.search(class_regex)>=0) 
	newinfo=classinfo.replace(class_regex,"");
      else {
	if (attrib)
	  elt.setAttribute(attrib,classinfo+' '+classname);
	else elt.className=classinfo+' '+classname;
	return true;}
      if (newinfo)
	newinfo=newinfo.
	  replace(whitespace_pat," ").
	  replace(trimspace_pat,"");
      if (attrib)
	if (newinfo) {
	  elt.setAttribute(attrib,newinfo);
	  elt.className=elt.className;}
	else if (!(keep)) {
	  elt.removeAttribute(attrib);
	  elt.className=elt.className;}
	else {}
      else elt.className=newinfo;
      return false;}
    fdjtDOM.toggleClass=toggleClass;
    fdjtDOM.tC=toggleClass;
	
    fdjtDOM.isTextInput=function(target){
      return ((target.tagName==='INPUT')||(target.tagName==='TEXTAREA'));};

    /* Simple CSS selectors */

    var selectors={};

    function Selector(spec,tagcs) {
      if (!(spec)) return this; // just cons with type
      else if (selectors[spec]) return selectors[spec]; // check cache
      else if (!(this instanceof Selector))
	// handle case of the forgotten 'new'
	return Selector.call(new Selector(),spec);
      if (spec.indexOf(',')>0) { // compound selectors
	var specs=spec.split(','); var compound=[];
	var i=0; var lim=specs.length;
	while (i<lim) {
	  var sub=string_trim(specs[i++]);
	  compound.push(new Selector(sub));}
	this.compound=compound;
	selectors[spec]=this;
	return this;}
      // Otherwise, parse and set up this
      var elts=spec.match(css_selector_regex);
      var i=0; var lim=elts.length;
      var classes=[]; var classnames=[]; var attribs=false;
      if (!((elts[0][0]==='.')||(elts[0][0]==='#')||(elts[0][0]==='['))) {
	this.tag=((tagcs)?(elts[0]):(elts[0].toUpperCase()));
	i=1;}
      while (i<lim)
	if (elts[i][0]==='#') this.id=elts[i++].slice(1);
	else if (elts[i][0]==='.') {
	  classnames.push(elts[i].slice(1));
	  classes.push(classPat(elts[i++].slice(1)));}
	else if (elts[i][0]==='[') {
	  var aelts=elts[i++]; var eltsend=aelts.length-1;
	  if (!(attribs)) attribs={};
	  var eqpos=aelts.indexOf('=');
	  if (eqpos<0)
	    attribs[aelts.slice(1,eltsend)]=true;
	  else if (aelts[eqpos+1]==='~') 
	    attribs[aelts.slice(1,eqpos)]=
	      classPat(aelts.slice(eqpos+2,eltsend));
	  else attribs[aelts.slice(1,eqpos)]=aelts.slice(eqpos+1,eltsend);}
	else fdjtLog.uhoh("weird elts %o",elts[i++]);
      if (classes.length) {
	this.classes=classes; this.classnames=classnames;}
      if (attribs) this.attribs=attribs;
      selectors[spec]=this;
      return this;}
    Selector.prototype.match=function(elt){
      if (this.compound) {
	var compound=this.compound; var i=0; var lim=compound.length;
	while (i<lim) if (compound[i++].match(elt)) return true;
	return false;} 
      if ((this.tag)&&(this.tag!==elt.tagName)) return false;
      else if ((this.id)&&(this.id!==elt.id)) return false;
      if (this.classes)
	if (elt.className) {
	  var classname=elt.className; var classes=this.classes;
	  var i=0; var lim=classes.length;
	  while (i<lim) if (classname.search(classes[i++])<0) return false;}
	else return false;
      if (this.attribs) {
	var attribs=this.attribs;
	for (var name in attribs) {
	  var val=elt.getAttribute(name);
	  if (!(val)) return false;
	  var need=this[name];
	  if (need===true) {}
	  else if (typeof need === 'string') {
	    if (need!==val) return false;}
	  else if (val.search(need)<0) return false;}}
      return true;};
    Selector.prototype.find=function(elt,results){
      var pickfirst=false;
      if (!(results)) results=[];
      if (this.compound) {
	var compound=this.compound; var i=0; var lim=compound.length;
	while (i<lim) compound[i++].find(elt,results);
	return results;}
      if (this.id) {
	var elt=document.getElementById(this.id);
	if (!(elt)) return results;
	else if (this.match(elt)) {
	  results.push(elt); return results;}
	else return results;}
      var candidates=[];
      var classnames=this.classnames; var attribs=this.attribs;
      if (this.classes) 
	if (elt.getElementsByClassName)
	  candidates=elt.getElementsByClassName(classnames[0]);
	else gatherByClass(elt,this.classes[0],candidates);
      else if ((this.tag)&&(elt.getElementsByTagName))
	candidates=elt.getElementsByTagName(this.tag);
      else if (this.attribs) {
	var attribs=this.attribs;
	for (var name in attribs) {
	  gatherByAttrib(elt,name,attribs[name],candidates);
	  break;}}
      else if (this.tag) {
	gatherByTag(elt,this.tag,candidates);}
      else {}
      if (candidates.length===0) return candidates;
      if (((this.tag)&&(!(this.classes))&&(!(this.attribs)))||
	  ((!(this.tag))&&(this.classes)&&(this.classes.length===1)&&
	   (!(this.attribs))))
	// When there's only one test, don't bother filtering
	if (results.length) return extendArray(results,candidates);
	else if (candidates instanceof Array)
	  return candidates;
	else return toArray(candidates);
      var i=0; var lim=candidates.length;
      while (i<lim) {
	var candidate=candidates[i++];
	if (this.match(candidate)) results.push(candidate);}
      return results;};
    fdjtDOM.Selector=Selector;
    fdjtDOM.sel=function(spec){
      if (!(spec)) return false;
      else if (spec instanceof Selector) return spec;
      else if (spec instanceof Array) {
	if (spec.length)
	  return new Selector(spec.join(","));
	else return false;}
      else if (typeof spec === 'string')
	return new Selector(spec);
      else {
	fdjtLog.warn("Non selector spec: %o",spec);
	return false;}};

    function gatherByClass(node,pat,results){
      if (node.nodeType===1) {
	if ((node.className)&&(node.className.search(pat)>=0))
	  results.push(node);
	var children=node.childNodes;
	if (children) {
	  var i=0; var lim=children.length; var result;
	  while (i<lim) gatherByClass(children[i++],pat,results);}}}
    function gatherByTag(node,tag,results){
      if (node.nodeType===1) {
	if (node.tagName===tag) results.push(node);
	var children=node.childNodes;
	if (children) {
	  var i=0; var lim=children.length; var result;
	  while (i<lim) gatherByTag(children[i++],tag,results);}}}
    function gatherByAttrib(node,attrib,val,results){
      if (node.nodeType===1) {
	if ((node.getAttribute(attrib))&&
	    ((typeof val === 'string')?
	     (node.getAttribute(attrib)===val):
	     (node.getAttribute(attrib).search(val)>=0)))
	  results.push(node);
	var children=node.childNodes;
	if (children) {
	  var i=0; var lim=children.length; var result;
	  while (i<lim) gatherByTag(children[i++],tag,results);}}}
	
    function gather_children(node,pat,attrib,results){
      if (!(attrib)) gatherByClass(node,pat,results);
      else if (attrib==='class') gatherByClass(node,pat,results);
      else if (attrib==='tagName') gatherByTag(node,pat,results);
      else gatherByAttrib(node,attrib,pat,results);}

    /* Real simple DOM search */

    function getParent(elt,parent,attrib){
      if (!(parent)) return false;
      else if (parent.nodeType) {
	while (elt) {
	  if (elt===parent) return parent;
	  else elt=elt.parentNode;}
	return false;}
      else if (typeof parent === 'function') {
	while (elt) {
	  if (parent(elt)) return elt;
	  else elt=elt.parentNode;}
	return false;}
      else if (parent instanceof Selector) {
	while (elt) {
	  if (parent.match(elt)) return elt;
	  else elt=elt.parentNode;}
	return false;}
      else if (typeof parent === 'string')
	return getParent(elt,new Selector(parent));
      else throw { error: 'invalid parent spec'};}
    fdjtDOM.getParent=getParent;
    fdjtDOM.hasParent=getParent;
    fdjtDOM.$P=getParent;
    fdjtDOM.inherits=function(node,spec) {
      var sel=new Selector(spec);
      return ((sel.match(node))?(node):(getParent(node,sel)));};

    function getChildren(node,classname,attrib,results){
      if (!(results)) results=[]; 
      if ((!(attrib))&&(typeof classname === 'function'))
	filter_children(node,classname,results);
      else if (!(attrib))
	if (classname instanceof Selector)
	  return classname.find(node,results);
	else if (typeof classname === 'string')
	  if ((usenative) && (node.querySelectorAll))
	    return node.querySelectorAll(classname);
	  else return getChildren(node,new Selector(classname),false,results);
	else throw { error: 'bad selector arg', selector: classname};
      else {
	var pat=(classpats[parent]||classPat(parent));
	gather_children(node,classname,attrib||false,results);}
      return results;}
    fdjtDOM.getChildren=getChildren;
    fdjtDOM.$=function(spec,root){
      return toArray(getChildren(root||document,spec));};
    fdjtDOM.getFirstChild=function(elt,spec){
      var children=getChildren(elt,spec);
      if (children.length) return children[0]; else return false;};
    fdjtDOM.getChild=fdjtDOM.getFirstChild;

    function filter_children(node,filter,results){
      if (node.nodeType===1) {
	if (filter(node)) results.push(node);
	var children=node.childNodes;
	if (children) {
	  var i=0; var lim=children.length; var result;
	  while (i<lim) filter_children(children[i++],filter,results);}}}

    fdjtDOM.getAttrib=function(elt,attrib,ns){
      var probe;
      if ((ns)&&(elt.getAttributeByNS))
	probe=elt.getAttributeNS(attrib,ns);
      if (probe) return probe;
      else return elt.getAttribute(attrib)||
	     elt.getAttribute("data-"+attrib);};

    fdjtDOM.findAttrib=function(scan,attrib,ns){
      var dattrib="data-"+attrib;
      while (scan) {
	if ((ns)&&(scan.getAttributeNS)&&
	    (scan.getAttributeNS(attrib,ns)))
	  return scan.getAttributeNS(attrib,ns);
	else if (scan.getAttribute) {
	  if (scan.getAttribute(attrib))
	    return scan.getAttribute(attrib);
	  else if (scan.getAttribute(dattrib))
	    return scan.getAttribute(dattrib);
	  else scan=scan.parentNode;}
	else scan=scan.parentNode;}
      return false;};
	    
    /* Manipulating the DOM */

    fdjtDOM.replace=function(existing,replacement){
      var cur=existing;
      if (typeof existing === 'string')
	if (existing[0]==='#')
	  cur=document.getElementById(existing.slice(1));
	else cur=document.getElementById(existing);
      if (cur) {
	cur.parentNode.replaceChild(replacement,cur);
	if ((cur.id)&&(!(replacement.id))) replacement.id=cur.id;}
      else fdjtLog.uhoh("Can't find %o to replace it with %o",
			existing,replacement);};
    function remove_node(node){
      if (node instanceof Array) {
	var i=0; var lim=node.length;
	while (i<lim) remove_node(node[i++]);
	return;}
      var cur=node;
      if (typeof node === 'string')
	if (node[0]==='#') cur=document.getElementById(node.slice(1));
	else cur=document.getElementById(node);
      if (cur) cur.parentNode.removeChild(cur);
      else fdjtLog.uhoh("Can't find %o to remove it",node);}
    fdjtDOM.remove=remove_node;
	
    fdjtDOM.append=function (node) {
      if (typeof node === 'string') node=document.getElementById(node);
      domappend(node,arguments,1);};
    fdjtDOM.prepend=function (node) {
      if (typeof node === 'string') node=document.getElementById(node);
      if (node.firstChild)
	dominsert(node.firstChild,arguments,1);
      else domappend(node,arguments,1);};

    fdjtDOM.insertBefore=function (before) {
      if (typeof before === 'string') before=document.getElementById(before);
      dominsert(before,arguments,1);};
    fdjtDOM.insertAfter=function (before) {
      if (typeof before === 'string') before=document.getElementById(before);
      if (before.nextSibling)
	dominsert(before.nextSibling,arguments,1);
      else domappend(before.parentNode,arguments,1);};
	
    /* DOM construction shortcuts */

    function tag_spec(spec,tag){
      if (!(spec)) return tag;
      else if (typeof spec === 'string') {
	var wordstart=spec.search(/\w/g);
	var puncstart=spec.search(/\W/g);
	if (puncstart<0) return tag+"."+spec;
	else if (wordstart!==0) return tag+spec;
	return spec;}
      else if (spec.tagName) return spec;
      else {
	spec.tagName=tag;
	return spec;}}

    fdjtDOM.Input=function(spec,name,value,title){
      if (spec.search(/\w/)!==0) spec='INPUT'+spec;
      var node=fdjtDOM(spec);
      node.name=name;
      if (value) node.value=value;
      if (title) node.title=title;
      return node;};
    fdjtDOM.Checkbox=function(name,value,checked){
      var node=fdjtDOM("INPUT");
      node.type="checkbox"
      node.name=name;
      if (value) node.value=value;
      if (checked) node.checked=true;
      else node.checked=false;
      return node;};
    fdjtDOM.Anchor=function(href,spec){
      spec=tag_spec(spec,"A");
      var node=fdjtDOM(spec); node.href=href;
      domappend(node,arguments,2);
      return node;};
    fdjtDOM.Image=function(src,spec,alt,title){
      spec=tag_spec(spec,"IMG");
      var node=fdjtDOM(spec); node.src=src;
      if (alt) node.alt=alt;
      if (title) node.title=title;
      domappend(node,arguments,4);
      return node;};

    function getInputs(root,name,type){
      var results=[];
      var inputs=root.getElementsByTagName('input');
      var i=0; var lim=inputs.length;
      while (i<lim) {
	if (((!(name))||(inputs[i].name===name))&&
	    ((!(type))||(inputs[i].type===type)))
	  results.push(inputs[i++]); 
	else i++;}
      if ((!type)||(type==='textarea')||(type==='text')) {
	var inputs=root.getElementsByTagName('textarea');
	var i=0; var lim=inputs.length;
	while (i<lim) {
	  if (((!(name))||(inputs[i].name===name))&&
	      ((!(type))||(inputs[i].type===type)))
	    results.push(inputs[i++]); 
	  else i++;}}
      if ((!type)||(type==='button')||(type==='submit')) {
	var inputs=root.getElementsByTagName('button');
	var i=0; var lim=inputs.length;
	while (i<lim) {
	  if (((!(name))||(inputs[i].name===name))&&
	      ((!(type))||(inputs[i].type===type)))
	    results.push(inputs[i++]); 
	  else i++;}}
      return results;}

    fdjtDOM.getInputs=getInputs;
    fdjtDOM.getInput=function(root,name,type){
      var results=getInputs(root,name||false,type||false);
      if ((results)&&(results.length===1))
	return results[0];
      else if ((results)&&(results.length)) {
	fdjtLog.warn(
		     "Ambiguous input reference name=%o type=%o under %o",
		     name,type,root);
	return results[0];}
      else return false;};
	
    function getInputValues(root,name){
      var results=[];
      var inputs=root.getElementsByTagName('input');
      var i=0; var lim=inputs.length;
      while (i<lim) {
	var input=inputs[i++];
	if (input.name!==name) continue;
	if ((input.type==='checkbox')||(input.type==='radio')) {
	  if (!(input.checked)) continue;}
	results.push(input.value);}
      return results;}
    fdjtDOM.getInputValues=getInputValues;

    /* Getting style information generally */

    function getStyle(elt,prop){
      if (typeof elt === 'string') elt=document.getElementById(elt);
      if (!(elt)) return elt;
      var style=
	((window.getComputedStyle)&&(window.getComputedStyle(elt,null)))||
	(elt.currentStyle);
      if (!(style)) return false;
      else if (prop) return style[prop];
      else return style;}
    fdjtDOM.getStyle=getStyle;

    function styleString(elt){
      var style=elt.style; var result;
      if (!(style)) return false;
      var i=0; var lim=style.length;
      if (lim===0) return false;
      while (i<lim) {
	var p=style[i];
	var v=style[p];
	if (i===0) result=fdjtString("%s: %o",p,v);
	else result=result+"; "+fdjtString("%s: %o",p,v);
	i++;}
      return result;}
    fdjtDOM.styleString=styleString;

    /* Getting display style */

    var display_styles={
      "DIV": "block","P": "block","BLOCKQUOTE":"block",
      "H1": "block","H2": "block","H3": "block","H4": "block",
      "H5": "block","H6": "block","H7": "block","H8": "block",
      "UL": "block","LI": "list-item",
      "DL": "block","DT": "list-item","DD": "list-item",
      "SPAN": "inline","EM": "inline","STRONG": "inline",
      "TT": "inline","DEFN": "inline","A": "inline",
      "TD": "table-cell","TR": "table-row",
      "TABLE": "table", "PRE": "preformatted"};

    function getDisplayStyle(elt){
      return (((window.getComputedStyle)&&(window.getComputedStyle(elt,null))&&
	       (window.getComputedStyle(elt,null).display))||
	      (display_styles[elt.tagName])||
	      "inline");}
    fdjtDOM.getDisplay=getDisplayStyle;

    /* Generating text from the DOM */

    function flatten(string){return string.replace(/\s+/," ");};

    function textify(arg,flat,inside){
      if (arg.text) return flatten(arg.text);
      else if (arg.nodeType)
	if (arg.nodeType===3) return arg.nodeValue;
	else if (arg.nodeType===1) {
	  var children=arg.childNodes;
	  var display_type=getDisplayStyle(arg);
	  var string=""; var suffix="";
	  // Figure out what suffix and prefix to use for this element
	  // If inside is false, don't use anything.
	  if (!(inside)) {}
	  else if (!(display_type)) {}
	  else if (display_type==="inline") {}
	  else if (flat) suffix=" ";
	  else if ((display_type==="block") ||
		   (display_type==="table") ||
		   (display_type==="preformatted")) {
	    string="\n"; suffix="\n";}
	  else if (display_type==="table-row") suffix="\n";
	  else if (display_type==="table-cell") string="\t";
	  else {}
	  var i=0; while (i<children.length) {
	    var child=children[i++];
	    if (!(child.nodeType)) continue;
	    if (child.nodeType===3)
	      if (flat)
		string=string+flatten(child.nodeValue);
	      else string=string+child.nodeValue;
	    else if (child.nodeType===1) {
	      var stringval=textify(child,flat,true);
	      if (stringval) string=string+stringval;}
	    else continue;}
	  return string+suffix;}
	else {}
      else if (arg.toString)
	return arg.toString();
      else return arg.toString();}
    fdjtDOM.textify=textify;

    /* Geometry functions */

    function getGeometry(elt,root,withstack){
      if (typeof elt === 'string')
	elt=document.getElementById(elt);
      var result={};
      var top = elt.offsetTop;
      var left = elt.offsetLeft;
      var stack = ((withstack) ? (new Array(elt)) : false);
      var width=elt.offsetWidth;
      var height=elt.offsetHeight;
	var rootp=((root)&&(root.offsetParent));

      if (elt===root) {
	result.left=0; result.top=0;
	result.width=width; result.height=height;
	return result;}
      elt=elt.offsetParent;
      while (elt) {
	  if ((root)&&((elt===root)||(elt===rootp))) break;
	if (withstack) withstack.push(elt);
	top += elt.offsetTop;
	left += elt.offsetLeft;
	elt=elt.offsetParent;}
	    
      result.left=left; result.top=top;
      result.width=width;
      result.height=height;
	    
      result.right=left+width; result.bottom=top+height;

      if (withstack) result.stack=withstack;

      return result;}
    fdjtDOM.getGeometry=getGeometry;

    function isVisible(elt,partial){
      var start=elt;
      if (!(partial)) partial=false;
      var top = elt.offsetTop;
      var left = elt.offsetLeft;
      var width = elt.offsetWidth;
      var height = elt.offsetHeight;
      var winx=(window.pageXOffset||document.documentElement.scrollLeft||0);
      var winy=(window.pageYOffset||document.documentElement.scrollTop||0);
      var winxedge=winx+(document.documentElement.clientWidth);
      var winyedge=winy+(document.documentElement.clientHeight);
	    
      while(elt.offsetParent) {
	if (elt===window) break;
	elt = elt.offsetParent;
	top += elt.offsetTop;
	left += elt.offsetLeft;}

      if ((elt)&&(!((elt===window)||(elt===document.body)))) {
	// fdjtLog("%o l=%o t=%o",elt,elt.scrollLeft,elt.scrollTop);
	if ((elt.scrollTop)||(elt.scrollLeft)) {
	  fdjtLog("Adjusting for inner DIV");
	  winx=elt.scrollLeft; winy=elt.scrollTop;
	  winxedge=winx+elt.scrollWidth;
	  winyedge=winy+elt.scrollHeight;}}

      /*
	fdjtLog("fdjtIsVisible%s %o top=%o left=%o height=%o width=%o",
	((partial)?("(partial)"):""),start,
	top,left,height,width);
	fdjtLog("fdjtIsVisible %o winx=%o winy=%o winxedge=%o winyedge=%o",
	elt,winx,winy,winxedge,winyedge);
      */
	    
      if (partial)
	// There are three cases we check for:
	return (
		// top of element in window
		((top > winy) && (top < winyedge) &&
		 (left > winx) && (left < winxedge)) ||
		// bottom of element in window
		((top+height > winy) && (top+height < winyedge) &&
		 (left+width > winx) && (left+width < winxedge)) ||
		// top above/left of window, bottom below/right of window
		(((top < winy) || (left < winx)) &&
		 ((top+height > winyedge) && (left+width > winxedge))));
      else return ((top > winy) && (left > winx) &&
		   (top + height) <= (winyedge) &&
		   (left + width) <= (winxedge));}
    fdjtDOM.isVisible=isVisible;

    function isAtTop(elt,delta){
      if (!(delta)) delta=50;
      var top = elt.offsetTop;
      var left = elt.offsetLeft;
      var width = elt.offsetWidth;
      var height = elt.offsetHeight;
      var winx=(window.pageXOffset||document.documentElement.scrollLeft||0);
      var winy=(window.pageYOffset||document.documentElement.scrollTop||0);
      var winxedge=winx+(document.documentElement.clientWidth);
      var winyedge=winy+(document.documentElement.clientHeight);
	    
      while(elt.offsetParent) {
	elt = elt.offsetParent;
	top += elt.offsetTop;
	left += elt.offsetLeft;}

      return ((top>winx) && (top<winyedge) && (top<winx+delta));}
    fdjtDOM.isAtTop=isAtTop;

    function textwidth(node){
      if (node.nodeType===3) return node.nodeValue.length;
      else if ((node.nodeType===1)&&(node.childNodes)) {
	var children=node.childNodes;
	var i=0; var lim=children.length; var width=0;
	while (i<lim) {
	  var child=children[i++];
	  if (child.nodeType===3) width=width+child.nodeValue.length;
	  else if (child.nodeType===1)
	    width=width+textwidth(child);
	  else {}}
	return width;}
      else return 0;}
    fdjtDOM.textWidth=textwidth;

    function hasContent(node,recur){
      if (node.childNodes) {
	var children=node.childNodes;
	var i=0; while (i<children.length) {
	  var child=children[i++];
	  if (child.nodeType===3)
	    if (child.nodeValue.search(/\w/g)>=0) return true;
	    else {}
	  else if ((recur) && (child.nodeType===1))
	    if (hasContent(child)) return true;
	    else {}}
	return false;}
      else return false;}
    fdjtDOM.hasContent=hasContent;

    function hasText(node){
      if (node.childNodes) {
	var children=node.childNodes;
	var i=0; while (i<children.length) {
	  var child=children[i++];
	  if (child.nodeType===3)
	    if (child.nodeValue.search(/\w/g)>=0) return true;
	    else {}}
	return false;}
      else return false;}
    fdjtDOM.hasText=hasText;

    /* A 'refresh method' does a className eigenop to force IE redisplay */

    fdjtDOM.refresh=function(elt){
      elt.className=elt.className;};
    fdjtDOM.setAttrib=function(elt,attrib,val){
      if ((typeof elt === 'string')&&(fdjtID(elt)))
	elt=fdjtID(elt);
      elt.setAttribute(attrib,val);
      elt.className=elt.className;};
    fdjtDOM.dropAttrib=function(elt,attrib){
      if ((typeof elt === 'string')&&(fdjtID(elt)))
	elt=fdjtID(elt);
      elt.removeAttribute(attrib);
      elt.className=elt.className;};

    /* Determining if something has overflowed */

    fdjtDOM.overflowing=function(node){
      // I haven't really tried this cross-browser, but I read it worked and
      //  have been in situations where it would be handy
      return (node.clientHeight!==node.scrollHeight);}

    /* Sizing to fit */

    var default_trace_adjust=false;

    function getInsideBounds(container){
      var left=false; var top=false;
      var right=false; var bottom=false;
      var children=container.childNodes;
      var i=0; var lim=children.length;
      while (i<lim) {
	var child=children[i++];
	if (typeof child.offsetLeft !== 'number') continue;
	var style=getStyle(child);
	if (style.position!=='static') continue;
	var child_left=child.offsetLeft-parsePX(style.marginLeft);
	var child_top=child.offsetTop-parsePX(style.marginTop);
	var child_right=child.offsetLeft+child.offsetWidth+parsePX(style.marginRight);
	var child_bottom=child.offsetTop+child.offsetHeight+parsePX(style.marginBottom);
	if (left===false) {
	  left=child_left; right=child_right;
	  top=child_top; bottom=child_bottom;}
	else {
	  if (child_left<left) left=child_left;
	  if (child_top<top) top=child_top;
	  if (child_right>right) right=child_right;
	  if (child_bottom>bottom) bottom=child_bottom;}}
      return {left: left,right: right,top: top, bottom: bottom,
	  width: right-left,height:bottom-top};}
    fdjtDOM.getInsideBounds=getInsideBounds;
    function applyScale(container,scale,traced){
      var images=fdjtDOM.getChildren(container,"IMG");
      var ilim=images.length;
      var oldscale=container.scale||100;
      if (scale) {
	container.scale=scale;
	container.style.fontSize=scale+'%';
	var rounded=10*Math.round(scale/10);
	fdjtDOM.addClass(container,"fdjtscaled");
	fdjtDOM.swapClass(
			  container,/\bfdjtscale\d+\b/,"fdjtscale"+rounded);}
      else if (!(container.scale)) return;
      else {
	delete container.scale;
	container.style.fontSize="";
	fdjtDOM.dropClass(container,"fdjtscaled");
	fdjtDOM.dropClass(container,/\bfdjtscale\d+\b/);}
      var iscan=0; while (iscan<ilim) {
	var image=images[iscan++];
	if ((fdjtDOM.hasClass(image,"nofdjtscale"))||
	    (fdjtDOM.hasClass(image,"noautoscale")))
	  continue;
	// Reset dimensions to get real info
	image.style.maxWidth=image.style.width=
	  image.style.maxHeight=image.style.height='';
	if (scale) {
	  var width=image.offsetWidth;
	  var height=image.offsetHeight;
	  image.style.maxWidth=image.style.width=
	    Math.round(width*(scale/100))+'px';
	  image.style.maxHeight=image.style.height=
	    Math.round(height*(scale/100))+'px';}}}
	
    function adjustToFit(container,threshold,padding){
      var trace_adjust=(container.traceadjust)||
	fdjtDOM.trace_adjust||
	((container.className)&&(container.className.search(/\btraceadjust\b/)>=0))||
	default_trace_adjust;
      var style=getStyle(container);
      var geom=getGeometry(container);
      var maxheight=((style.maxHeight)&&(parsePX(style.maxHeight)))||
	(geom.height);
      var maxwidth=((style.maxWidth)&&(parsePX(style.maxWidth)))||
	(geom.width);
      var goodenough=threshold||0.1;
      var scale=(container.scale)||100.0;
      var bounds=getInsideBounds(container);
      var hpadding=
	(fdjtDOM.parsePX(style.paddingLeft)||0)+
	(fdjtDOM.parsePX(style.paddingRight)||0)+
	(fdjtDOM.parsePX(style.borderLeftWidth)||0)+
	(fdjtDOM.parsePX(style.borderRightWidth)||0);
      var vpadding=
	(fdjtDOM.parsePX(style.paddingTop)||0)+
	(fdjtDOM.parsePX(style.paddingBottom)||0)+
	(fdjtDOM.parsePX(style.borderTopWidth)||0)+
	(fdjtDOM.parsePX(style.borderBottomWidth)||0);
      maxwidth=maxwidth-hpadding; maxheight=maxheight-vpadding; 
      var itfits=((bounds.height/maxheight)<=1)&&((bounds.width/maxwidth)<=1);
      if (trace_adjust) 
	fdjtLog("Adjust (%o) %s cur=%o%s, best=%o~%o, limit=%ox%o=%o, box=%ox%o=%o, style=%s",
		goodenough,fdjtDOM.nodeString(container),
		scale,((itfits)?" (fits)":""),
		container.bestscale||-1,container.bestfit||-1,
		maxwidth,maxheight,maxwidth*maxheight,
		bounds.width,bounds.height,bounds.width*bounds.height,
		styleString(container));
      if (itfits) {
	/* Figure out how well it fits */
	var fit=Math.max((1-(bounds.width/maxwidth)),
			 (1-(bounds.height/maxheight)));
	var bestfit=container.bestfit||1.5;
	if (!(trace_adjust)) {}
	else if (container.bestscale) 
	  fdjtLog("%s %o~%o vs. %o~%o",
		  ((fit<goodenough)?"Good enough!":
		   ((fit<bestfit)?"Better!":"Worse!")),
		  scale,fit,container.bestscale,container.bestfit);
	else fdjtLog("First fit %o~%o",scale,fit);
	if (fit<bestfit) {
	  container.bestscale=scale; container.bestfit=fit;}
	// If it's good enough, just return
	if (fit<goodenough) {
	  container.goodscale=scale; return;}}
      // Figure out the next scale factor to try
      var dh=bounds.height-maxheight; var dw=bounds.width-maxwidth;
      var rh=maxheight/bounds.height; var rw=maxwidth/bounds.width;
      var newscale=
	((itfits)?
	 (scale*Math.sqrt
	  ((maxwidth*maxheight)/(bounds.width*bounds.height))):
	 (rh<rw)?(scale*rh):(scale*rw));
      if (trace_adjust)
	fdjtLog("[%fs] Trying newscale=%o, rw=%o rh=%o",
		fdjtET(),newscale,rw,rh);
      applyScale(container,newscale,trace_adjust);}
    fdjtDOM.applyScale=applyScale;
    fdjtDOM.adjustToFit=adjustToFit;
    fdjtDOM.insideBounds=getInsideBounds;
    fdjtDOM.finishScale=function(container){
      var traced=(container.traceadjust)||
      fdjtDOM.trace_adjust||default_trace_adjust;
      if (!(container.bestscale)) {
	applyScale(container,false,traced);
	fdjtLog("No good scaling for %o style=%s",
		fdjtDOM.nodeString(container),
		fdjtDOM.styleString(container));
	return;}
      else if (container.scale===container.bestscale) {}
      else applyScale(container,container.bestscale,traced);
      if (traced)
	fdjtLog("Final scale %o~%o for %o style=%s",
		container.bestscale,container.bestfit,
		fdjtDOM.nodeString(container),
		fdjtDOM.styleString(container));
      delete container.bestscale;
      delete container.bestfit;
      delete container.goodscale;};
	
    /* Getting various kinds of metadata */

    function getHTML(){
      var children=document.childNodes;
      var i=0; var lim=children.length;
      while (i<lim)
	if (children[i].tagName==='HTML') return children[i];
	else i++;
      return false;}
    fdjtDOM.getHTML=getHTML;

    function getHEAD(){
      var children=document.childNodes;
      var i=0; var lim=children.length;
      while (i<lim)
	if (children[i].tagName==='HTML') {
	  var grandchildren=children[i].childNodes;
	  i=0; lim=grandchildren.length;
	  while (i<lim)
	    if (grandchildren[i].tagName==='HEAD')
	      return grandchildren[i];
	    else i++;
	  return false;}
	else i++;
      return false;}
    fdjtDOM.getHEAD=getHEAD;

    function getMeta(name,multiple,matchcase,dom){
      var results=[];
      var matchname=((!(matchcase))&&(name.toUpperCase()));
      var elts=((document.getElementsByTagName)?
		(document.getElementsByTagName("META")):
		(getChildren(document,"META")));
      var i=0; while (i<elts.length) {
	if (elts[i])
	  if ((elts[i].name===name)||
	      ((matchname)&&(elts[i].name)&&
	       (elts[i].name.toUpperCase()===matchname))) {
	    if (multiple) {
	      if (dom) results.push(elts[i++]);
	      else results.push(elts[i++].content);}
	    else if (dom) return elts[i];
	    else return elts[i].content;}
	  else i++;}
      if (multiple) return results;
      else return false;}
    fdjtDOM.getMeta=getMeta;

    // This gets a LINK href field
    function getLink(name,multiple,matchcase,dom){
      var results=[];
      var matchname=((!((matchcase)))&&(name.toUpperCase()));
      var elts=((document.getElementsByTagName)?
		(document.getElementsByTagName("LINK")):
		(getChildren(document,"LINK")));
      var i=0; while (i<elts.length) {
	if (elts[i])
	  if ((elts[i].rel===name)||
	      ((matchname)&&(elts[i].rel)&&
	       (elts[i].rel.toUpperCase()===matchname))) {
	    if (multiple) {
	      if (dom) results.push(elts[i++]);
	      else results.push(elts[i++].href);}
	    else if (dom) return elts[i];
	    else return elts[i].href;}
	  else i++;}
      if (multiple) return results;
      else return false;}
    fdjtDOM.getLink=getLink;

    /* Going forward */

    var havechildren=((document)&&
		      (document.body)&&
		      (document.body.childNodes)&&
		      (document.body.children));

    // NEXT goes to the next sibling or the parent's next sibling
    function next_node(node){
      while (node) {
	if (node.nextSibling)
	  return node.nextSibling;
	else node=node.parentNode;}
      return false;}
    function next_element(node){
      if (node.nextElementSibling)
	return node.nextElementSibling;
      else {
	var scan=node;
	while (scan=scan.nextSibling) {
	  if (!(scan)) return null;
	  else if (scan.nodeType===1) break;
	  else {}}
	return scan;}}
    function scan_next(node,test,justelts){
      if (!(test))
	if (justelts) {
	  if (havechildren) return node.nextElementSibling;
	  else return next_element(node);}
	else return next_node(node);
      var scan=((justelts)?
		((havechildren)?
		 (node.nextElementSibling):(next_element(node))):
		((node.nextSibling)||(next_node(node))));
      while (scan)
	if (test(scan)) return scan;
	else if (justelts)
	  scan=((scan.nextElementSibling)||(next_element(scan)));
	else scan=((scan.nextSibling)||(next_node(scan)));
      return false;}

    // FORWARD goes to the first deepest child
    function forward_node(node){
      if ((node.childNodes)&&((node.childNodes.length)>0))
	return node.childNodes[0];
      else while (node) {
	  if (node.nextSibling)
	    return node.nextSibling;
	  else node=node.parentNode;}
      return false;}
    function forward_element(node,n){
      var scan;
      if (n) {
	var i=0; scan=node;
	while (i<n) {scan=forward_element(scan); i++;}
	return scan;}
      if (havechildren) {
	if ((node.children)&&(node.children.length>0)) {
	  return node.children[0];}
	if (scan=node.nextElementSibling) return scan;
	while (node=node.parentNode)
	  if (scan=node.nextElementSibling) return scan;
	return false;}
      else {
	if (node.childNodes) {
	  var children=node.childNodes; var i=0; var lim=children.length;
	  while (i<lim)
	    if ((scan=children[i++])&&(scan.nodeType===1)) return scan;}
	while (scan=node.nextSibling) if (scan.nodeType===1) return scan;
	while (node=node.parentNode)
	  if (scan=next_element(node)) return scan;
	return false;}}
    function scan_forward(node,test,justelts){
      if (!(test)) {
	if (justelts) return forward_element(node);
	else return forward_node(node);}
      var scan=((justelts)?(forward_element(node)):(forward_node(node)));
      while (scan) {
	if (test(scan)) return scan;
	else if (justelts) scan=next_element(scan);
	else scan=next_node(scan);}
      return false;}

    fdjtDOM.nextElt=next_element;
    fdjtDOM.forwardElt=forward_element;
    fdjtDOM.forward=scan_forward;
    fdjtDOM.next=scan_next;

    /* Scanning backwards */

    // PREV goes the parent if there's no previous sibling
    function prev_node(node){
      while (node) {
	if (node.previousSibling)
	  return node.previousSibling;
	else node=node.parentNode;}
      return false;}
    function previous_element(node){
      if (havechildren)
	return node.previousElementSibling;
      else {
	var scan=node;
	while (scan=scan.previousSibling) 
	  if (!(scan)) return null;
	  else if (scan.nodeType===1) break;
	  else {}
	if (scan) return scan;
	else return scan.parentNode;}}
    function scan_previous(node,test,justelts){
      if (!(test))
	if (justelts) {
	  if (havechildren) return node.previousElementSibling;
	  else return previous_element(node);}
	else return previous_node(node);
      var scan=((justelts)?
		((havechildren)?(node.previousElementSibling):
		 (previous_element(node))):
		(previous_node(node)));
      while (scan)
	if (test(scan)) return scan;
	else if (justelts)
	  scan=((havechildren)?(scan.previousElementSibling):(previous_element(scan)));
	else scan=prev_node(scan);
      return false;}

    // BACKWARD goes to the final (deepest last) child
    //  of the previous sibling
    function backward_node(node){
      if (node.previousSibling) {
	var scan=node.previousSibling;
	// If it's not an element, just return it
	if (scan.nodeType!==1) return scan;
	// Otherwise, return the last and deepest child
	while (scan) {
	  var children=scan.childNodes;
	  if (!(children)) return scan;
	  else if (children.length===0) return scan;
	  else scan=children[children.length-1];}
	return scan;}
      else return node.parentNode;}

    function backward_element(node){
      if (havechildren)
	return ((node.previousElementSibling)?
		(get_final_child((node.previousElementSibling))):
		(node.parentNode));
      else if ((node.previousElementSibling)||(node.previousSibling)) {
	var start=(node.previousElementSibling)||(node.previousSibling);
	if (start.nodeType===1) 
	  return get_final_child(start);
	else return start;}
      else return node.parentNode;}
    // We use a helper function because 
    function get_final_child(node){
      if (node.nodeType===1) {
	if (node.childNodes) {
	  var children=node.childNodes;
	  if (!(children.length)) return node;
	  var scan=children.length-1;
	  while (scan>=0) {
	    var child=get_final_child(children[scan--]);
	    if (child) return child;}
	  return node;}
	else return node;}
      else return false;}
	
    function scan_backward(node,test,justelts){
      if (!(test)) {
	if (justelts) return backward_element(node);
	else return backward_node(node);}
      var scan=((justelts)?
		(backward_element(node)):
		(backward_node(node)));
      while (scan) {
	if (test(scan)) return scan;
	else if (justelts) scan=next_element(scan);
	else scan=next_node(scan);}
      return false;}
	
    fdjtDOM.prevElt=previous_element;
    fdjtDOM.backwardElt=backward_element;
    fdjtDOM.backward=scan_backward;
    fdjtDOM.prev=scan_previous;

    /* Viewport/window functions */

    fdjtDOM.viewTop=function(win){
      win=win||window;
      return (win.pageYOffset||win.scrollY||
	      win.document.documentElement.scrollTop||0);};
    fdjtDOM.viewLeft=function(win){
      win=win||window;
      return (win.pageXOffset||win.scrollX||
	      win.document.documentElement.scrollLeft||0);};
    fdjtDOM.viewHeight=function(win){
      win=win||window;
      var docelt=((win.document)&&(win.document.documentElement));
      return (win.innerHeight)||((docelt)&&(docelt.clientHeight));};
    fdjtDOM.viewWidth=function(win){
      win=win||window;
      var docelt=((win.document)&&(win.document.documentElement));
      return ((win.innerWidth)||((docelt)&&(docelt.clientWidth)));};

    /* Listeners (should be in UI?) */

    function addListener(node,evtype,handler){
      if (!(node)) node=document;
      if (typeof node === 'string') node=fdjtID(node);
      else if (node instanceof Array) {
	var i=0; var lim=node.length;
	while (i<lim) addListener(node[i++],evtype,handler);
	return;}
      else if (node.length)
	return addListener(TOA(node),evtype,handler);
      // OK, actually do it
      if (evtype==='title') { 
	// Not really a listener, but helpful
	if (typeof handler === 'string') 
	  if (node.title)
	    node.title='('+handler+') '+node.title;
	  else node.title=handler;}
      else if (evtype[0]==='=')
	node[evtype.slice(1)]=handler;
      else if (node.addEventListener)  {
	// fdjtLog("Adding listener %o for %o to %o",handler,evtype,node);
	return node.addEventListener(evtype,handler,false);}
      else if (node.attachEvent)
	return node.attachEvent('on'+evtype,handler);
      else fdjtLog.warn('This node never listens: %o',node);}
    fdjtDOM.addListener=addListener;

    function addListeners(node,handlers){
      if (handlers) 
	for (evtype in handlers) {
	  if (handlers[evtype])
	    addListener(node,evtype,handlers[evtype]);}}
    fdjtDOM.addListeners=addListeners;

    fdjtDOM.T=function(evt) {
      evt=evt||event; return (evt.target)||(evt.srcElement);};

    fdjtDOM.cancel=function(evt){
      evt=evt||event;
      if (evt.preventDefault) evt.preventDefault();
      else evt.returnValue=false;
      evt.cancelBubble=true;};

    fdjtDOM.init=function(){
      havechildren=((document)&&
		    (document.body)&&
		    (document.body.childNodes)&&
		    (document.body.children));};

    if (navigator.userAgent.search("WebKit")>=0) {
      fdjtDOM.transition='-webkit-transition';
      fdjtDOM.transitionProperty='-webkit-transition-property';
      fdjtDOM.transform='-webkit-transform';
      fdjtDOM.columnWidth='-webkit-column-width';
      fdjtDOM.columnGap='-webkit-column-gap';}
    else if (navigator.userAgent.search("Mozilla")>=0) {
      fdjtDOM.transitionProperty='-moz-transition-property';
      fdjtDOM.transition='-moz-transition';
      fdjtDOM.transform='-moz-transform';
      fdjtDOM.columnWidth='MozColumnWidth';
      fdjtDOM.columnGap='MozColumnGap';}
    else {
      fdjtDOM.transitionProperty='transition-property';
      fdjtDOM.transition='transition';
      fdjtDOM.transform='transform';}
	

    return fdjtDOM;
  })();

function fdjtID(id) {
    return ((id)&&
	    ((document.getElementById(id))||
	     ((id[0]==='#')&&
	      (document.getElementById(id.slice(1))))));}
function _(string) { return string;}

/* Emacs local variables
   ;;;  Local variables: ***
   ;;;  compile-command: "make; if test -f ../makefile; then cd ..; make; fi" ***
   ;;;  End: ***
*/
/* -*- Mode: Javascript; -*- */

var fdjt_kb_id="$Id$";
var fdjt_kb_version=parseInt("$Revision$".slice(10,-1));
fdjt_versions.decl("fdjt/kb",fdjt_kb_version);
fdjt_versions.decl("fdjt",fdjt_kb_version);

/* Copyright (C) 2009-2011 beingmeta, inc.
   This file is a part of the FDJT web toolkit (www.fdjt.org)
   This file provides extended Javascript utility functions
    of various kinds.

   This program comes with absolutely NO WARRANTY, including implied
   warranties of merchantability or fitness for any particular
   purpose.

    Use, modification, and redistribution of this program is permitted
    under either the GNU General Public License (GPL) Version 2 (or
    any later version) or under the GNU Lesser General Public License
    (version 3 or later).

    These licenses may be found at www.gnu.org, particularly:
      http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
      http://www.gnu.org/licenses/lgpl-3.0-standalone.html

*/

/*
  _fdjtid: unique integer assigned to objects
  fdjtKB.register (assigns unique ID)
  fdjtKB.Pool (creates a pool of named objects)
  fdjtKB.Set (creates a sorted array for set operations)
  fdjtKB.Ref (objects created within a pool)
 */

var fdjtKB=
    (function(){
	// This is the top level object/module 
	fdjtKB={};
	fdjtKB.revid="$Id$";
	fdjtKB.version=parseInt("$Revision$".slice(10,-1));
	fdjtKB.persist=((window.localStorage)?(true):(false));

	// This turns on debugging, which is further controlled
	//  by properties on pools
	var debug=false;
	fdjtKB.setDebug=function(flag){debug=flag;};

	// This checks if a reference is a 'real object'
	// I.E., something which shouldn't be used as a key
	//  or fast set member and not an array either
	var arrayobjs=(typeof new Array(1,2,3) === 'object');
	var stringobjs=(typeof new String() === 'object');
	function isobject(x){
	  return ((typeof x === 'object')&&
		  (!((arrayobjs)&&(x instanceof Array))));}
	function objectkey(x){
	  if (typeof x !== 'object') return x;
	  else if (x instanceof String) return x.toString();
	  else return x.qid||x.oid||x.uuid||x._fdjtid||register(x);}
	fdjtKB.objectkey=objectkey;
	fdjtKB.isobject=isobject;
	

	// We allocate 16 million IDs for miscellaneous objects
	//  and use counter to track them.
	var counter=0;
	function register(x){
	    return (x._fdjtid)||(x._fdjtid=(++counter));}
	fdjtKB.register=register;

	// This lets us figure out what inits were run in this session.
	var init_start=fdjtTime();

	// Pools are uniquely named id->object mappings
	// This table maps those unique names to the objects themselves
	// Pools can have aliases, so the name->pool mapping is many to one
	var pools={};

	function Pool(name) {
	    if (!(name)) return this;
	    if (pools[name]) return pools[name];
	    pools[name]=this; this.name=name; this.map={};
	    this.index=false; this.storage=false;
	    this.inits=false; this.effects=false; this.xforms={};
	    this.absref=false; // Whether names in this pool are 'absolute'
	    return this;}
	fdjtKB.Pool=Pool;
	fdjtKB.PoolRef=Pool;
	
	Pool.prototype.toJSON=function(){return "@@"+this.name;};
	
	// Check if a named pool exists
	Pool.probe=function(id) {return pools[id]||false;};

	Pool.prototype.addAlias=function(name) {
	    if (pools[name])
		if (pools[name]===this) return this;
	    else throw {error: "pool alias conflict"};
	    else pools[name]=this;};

	Pool.prototype.addEffect=function(prop,handler) {
	    if (!(this.effects)) this.effects={};
	    this.effects[prop]=handler;};
	Pool.prototype.addInit=function(handler) {
	    if (!(this.inits)) this.inits=[];
	    this.inits.push(handler);};

	Pool.prototype.probe=function(id) {
	    if (this.map[id]) return (this.map[id]);
	    else return false;};

	Pool.prototype.load=function(ref) {
	  if (typeof ref==='string')
	    return this.ref(ref).load();
	  else return ref.load();};

	Pool.prototype.ref=function(qid,cons) {
	    if (qid instanceof Ref) return qid;
	    if (this.map[qid]) return this.map[qid];
	    if (!(cons)) cons=this.cons(qid);
	    else if (cons instanceof Ref) {}
	    else cons=this.cons(qid);
	    if (!(cons.qid)) cons.qid=qid;
	    this.map[qid]=cons; cons.pool=this;
	    return cons;};
	Pool.prototype.drop=function(qid) {
	    var val=this.map[qid];
	    if ((val)&&(val.ondrop)) val.ondrop();
	    if (this.storage) this.storage.drop(val);
	    if (!(val)) return;
	    delete this.map[qid];
	    if (val.uuid) delete this.map[val.uuid];
	    if (val.oid) delete this.map[val.oid];}
	
	Pool.prototype.Import=function(data) {
	    if (data instanceof Array) {
		var i=0; var lim=data.length; var results=[];
		while (i<lim) results.push(this.Import(data[i++]));
		return;}
	    else {
		var qid=data.qid||data.oid||data.uuid;
		if ((debug)&&(this.traceimport))
		  fdjtLog("[%fs] Import to %s %o <== %o",
			  fdjtET(),this.name,obj,data);
		if (this.storage) this.storage.Import(data);
		if (qid) {
		    var obj=(this.map[qid]);
		    if (obj) obj.update(data);
		    else {
			obj=this.ref(qid);
			obj.init(data);}
		    return obj;}
		else return data;}};
	
	Pool.prototype.find=function(prop,val){
	    if (!(this.index)) return [];
	    return this.index(false,prop,val);};

	var uuid_pattern=
	    /[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}/;
	var refmaps=[];
	fdjtKB.addRefMap=function(map){
	  var i=0; var lim=refmaps.length;
	  while (i<lim) if (refmaps[i++]===map) return false;
	  refmaps.push(map);
	  return refmaps.length;};

	function getPool(arg){
	    var atpos; 
	    if (arg instanceof Ref) return arg.pool;
	    else if (typeof arg === 'number') return false;
	    else if (typeof arg === 'string') {
		var ref=parseRef(arg);
		if (ref) return ref.pool;
		else return false;}
	    else return false;}
	fdjtKB.getPool=getPool;

	function parseRef(arg,kno){
	    if (((arg[0]===':')&&(arg[1]==='@'))&&
		(((slash=arg.indexOf('/',2))>=0)))  {
		var pool=fdjtKB.PoolRef(arg.slice(1,slash+1));
		return pool.ref(arg);}
	    else if (((arg[0]==='@'))&&
		     (((slash=arg.indexOf('/',2))>=0)))  {
		var pool=fdjtKB.PoolRef(arg.slice(0,slash+1));
		return pool.ref(arg);}
	    else if ((atpos=arg.indexOf('@'))>1)  {
		var pool=fdjtKB.PoolRef(arg.slice(atpos+1));
		return pool.ref(arg.slice(0,atpos));}
	    else if (arg.search(uuid_pattern)===0) {
		var uuid_type=arg.slice(34);
		var pool=fdjtKB.PoolRef("-UUIDTYPE="+uuid_type);
		return pool.ref(arg);}
	    else if ((arg[0]===':')&&(arg[1]==='#')&&(arg[2]==='U')&&
		     (arg.search(uuid_pattern)===3)) {
		var uuid_type=arg.slice(37);
		var pool=fdjtKB.PoolRef("-UUIDTYPE="+uuid_type);
		return pool.ref(arg.slice(3));}
	    else if (refmaps.length) {
		var i=0; var lim=refmaps.length;
		while (i<lim) {
		    var refmap=refmaps[i++];
		    var ref=((typeof refmap === 'function')?
			     (refmap(arg)):(refmap[arg]));
		    if (ref) return ref;}
		return false;}
	    else if (kno) return kno.ref(arg);
	    else return false;}

	function getRef(arg,kno){
	    if (!(arg)) return false;
	    else if (arg instanceof Ref) return arg;
	    else if (typeof arg === 'number') return false;
	    else if (typeof arg === 'string')
		return parseRef(arg,kno);
	    else return false;}
	fdjtKB.ref=fdjtKB.getRef=getRef;
	function loadRef(arg){
	  var obj=getRef(arg);
	  if (obj) return obj.load();
	  else return undefined;}
	fdjtKB.load=fdjtKB.loadRef=loadRef;
	
	function doimport(data){
	    if (data instanceof Array) {
		var i=0; var lim=data.length; var results=[];
		while (i<lim) results.push(doimport(data[i++]));
		return results;}
	    else {
		var qid=data.qid||data.uuid||data.oid;
		if (qid) {
		    var pool=getPool(qid);
		    if (pool) return pool.Import(data);
		    else return data;}
		else return data;}}
	fdjtKB.Import=doimport;

	// Array utility functions
	function contains(arr,val,start){
	    if (arr.indexOf)
		return (arr.indexOf(val,start)>=0);
	    var i=start||0; var len=arr.length;
	    while (i<len)
		if (arr[i]===val) return true;
	    else i++;
	    return false;}
	function position(arr,val,start){
	    if (arr.indexOf)
		return arr.indexOf(val,start);
	    var i=start||0; var len=arr.length;
	    while (i<len)
		if (arr[i]===val) return i;
	    else i++;
	    return -1;}

	/* Fast sets */
	function set_sortfn(a,b) {
	    if (a===b) return 0;
	    else if (typeof a === typeof b) {
		if (typeof a === "number")
		    return a-b;
		else if (typeof a === "string")
		    if (a<b) return -1;
		else return 1;
		else if (a.qid)
		    if (b.qid)
			if (a.qid<b.qid) return -1;
		else if (a.qid===b.qid) return 0;
		else return 1;
		else return 1;
		else if (b.qid) return -1;
		else if (a._fdjtid)
		    if (b._fdjtid) return a._fdjtid-b._fdjtid;
		else {
		    b._fdjtid=++counter;
		    return -1;}
		else if (b._fdjtid) {
		    a._fdjtid=++counter;
		    return 1;}
		else {
		    a._fdjtid=++counter;
		    b._fdjtid=++counter;
		    return -1;}}
	    else if (typeof a < typeof b) return -1;
	    else return 1;
	}

	function length_sortfn(a,b) {
	    if (a.length===b.length) return 0;
	    else if (a.length<b.length) return -1;
	    else return 1;}

	function intersection(set1,set2){
	    if ((!(set1))||(set1.length===0)) return [];
	    if ((!(set2))||(set2.length===0)) return [];
	    var results=new Array();
	    var i=0; var j=0; var len1=set1.length; var len2=set2.length;
	    var allstrings=set1._allstrings&&set2._allstrings;
	    var new_allstrings=true;
	    while ((i<len1) && (j<len2))
		if (set1[i]===set2[j]) {
		    if ((new_allstrings)&&(typeof set1[i] !== 'string'))
			new_allstrings=false;
		    results.push(set1[i]);
		    i++; j++;}
	    else if ((allstrings)?
		     (set1[i]<set2[j]):
		     (set_sortfn(set1[i],set2[j])<0)) i++;
	    else j++;
	    results._allstrings=new_allstrings;
	    results._sortlen=results.length;
	    return results;}
	fdjtKB.intersection=intersection;
	
	function union(set1,set2){
	    if ((!(set1))||(set1.length===0)) return set2;
	    if ((!(set2))||(set2.length===0)) return set1;
	    var results=new Array();
	    var i=0; var j=0; var len1=set1.length; var len2=set2.length;
	    var allstrings=set1._allstrings&&set2._allstrings;
	    while ((i<len1) && (j<len2))
		if (set1[i]===set2[j]) {
		    results.push(set1[i]); i++; j++;}
	    else if ((allstrings)?
		     (set1[i]<set2[j]):
		     (set_sortfn(set1[i],set2[j])<0))
		results.push(set1[i++]);
	    else results.push(set2[j++]);
	    while (i<len1) results.push(set1[i++]);
	    while (j<len2) results.push(set2[j++]);
	    results._allstrings=allstrings;
	    results._sortlen=results.length;
	    return results;}
	fdjtKB.union=union;

	function merge(set1,set2){
	    if ((!(set1))||(set1.length===0)) {
		set1.concat(set2);
		set1._sortlen=set2._sortlen;
		set1._allstrings=set2._allstrings;
		return set1;}
	    if ((!(set2))||(set2.length===0)) return set1;
	    var results=set1;
	    set1=[].concat(results);
	    var i=0; var j=0; var len1=set1.length; var len2=set2.length;
	    var allstrings=set1._allstrings&&set2._allstrings;
	    while ((i<len1) && (j<len2))
		if (set1[i]===set2[j]) {
		    results.push(set1[i]); i++; j++;}
	    else if ((allstrings)?
		     (set1[i]<set2[j]):
		     (set_sortfn(set1[i],set2[j])<0))
		results.push(set1[i++]);
	    else results.push(set2[j++]);
	    while (i<len1) results.push(set1[i++]);
	    while (j<len2) results.push(set2[j++]);
	    results._allstrings=allstrings;
	    results._sortlen=results.length;
	    return results;}
	fdjtKB.merge=merge;

	function overlaps(set1,set2){
	    if ((!(set1))||(set1.length===0)) return [];
	    if ((!(set2))||(set2.length===0)) return [];
	    var results=new Array();
	    var i=0; var j=0; var len1=set1.length; var len2=set2.length;
	    var allstrings=set1._allstrings&&set2._allstrings;
	    var new_allstrings=true;
	    while ((i<len1) && (j<len2))
		if (set1[i]===set2[j]) return true;
	    else if ((allstrings)?
		     (set1[i]<set2[j]):
		     (set_sortfn(set1[i],set2[j])<0)) i++;
	    else j++;
	    return false;}
	fdjtKB.overlaps=overlaps;

	/* Sets */
	/* sets are really arrays that are sorted to simplify set operations.
	   the ._sortlen property tells how much of the array is sorted */
	function Set(arg){
	    if (arguments.length===0) return [];
	    else if (arguments.length===1) {
		if (!(arg)) return [];
		else if (arg instanceof Array) {
		    if ((!(arg.length))||(arg._sortlen===arg.length))
			return arg;
		    else if (arg._sortlen) return setify(arg);
		    else return setify([].concat(arg));}
		else {
		    var result=[arg]; 
		    if (typeof arg === 'string') result._allstrings=true;
		    result._sortlen=1;
		    return result;}}
	    else {
		var result=[];
		for (arg in arguments)
		    if (!(arg)) {}
		else if (arg instanceof Array) result.concat(arg);
		else result.push(arg);
		return setify(result);}}
	fdjtKB.Set=Set;

	function setify(array) {
	    if (array._sortlen===array.length) return array;
	    // else if ((array._sortlen)&&(array._sortlen>1))
	    else if (array.length===0) return array;
	    else {
		var allstrings=true;
		for (elt in array)
		    if (typeof elt !== 'string') {allstrings=false; break;}
		array._allstrings=allstrings;
		if (allstrings) array.sort();
		else array.sort(set_sortfn);
		var read=1; var write=1; var lim=array.length;
		var cur=array[0];
		while (read<lim)
		    if (array[read]!==cur) {
			cur=array[read++]; write++;}
		else read++;
		array._sortlen=array.length=write;
		return array;}}
	
	fdjtKB.contains=contains;
	fdjtKB.position=position;
	
	function set_add(set,val) {
	    if (val instanceof Array) {
		var changed=false;
		for (elt in val) 
		    if (set_add(set,elt)) changed=true;
		return changed;}
	    else if (set.indexOf) {
		var pos=set.indexOf(val);
		if (pos>=0) return false;
		else set.push(val);
		return true;}
	    else {
		var i=0; var lim=set.length;
		while (i<lim)
		    if (set[i]===val) return false; else i++;
		if (typeof val !== 'string') set._allstrings=false;
		set.push(val);
		return true;}}
	
	function set_drop(set,val) {
	    if (val instanceof Array) {
		var changed=false;
		for (elt in val)
		    if (set_drop(set,elt)) changed=true;
		return changed;}
	    else if (set.indexOf) {
		var pos=set.indexOf(val);
		if (pos<0) return false;
		else set.splice(pos,1);
		return true;}
	    else {
		var i=0; var lim=set.length;
		while (i<lim)
		    if (set[i]===val) {
			array.splice(i,1);
			return true;}
		else i++;
		return false;}}
	
	/* Maps */
	function Map() {
	    this.scalar_map={}; this.object_map={};
	    return this;}
	Map.prototype.get=function(key) {
	  if (isobject(key))
	    return this.object_map
	      [key.qid||key.oid||key.uuid||key._fdjtid||register(key)];
	  else return this.scalar_map[key];};
	Map.prototype.set=function(key,val) {
	  if (isobject(key))
	    this.object_map
	      [key.qid||key.oid||key.uuid||key._fdjtid||register(key)]=val;
	  else this.scalar_map[key]=val;};
	Map.prototype.add=function(key,val) {
	  if (isobject(key)) {
	    var objkey=key.qid||key.oid||key.uuid||key._fdjtid||
	    register(key);
	    var cur=this.object_map[objkey];
	    if (!(cur)) {
	      this.object_map[objkey]=[val];
	      return true;}
	    else if (!(cur instanceof Array)) {
	      if (cur===val) return false;
	      else {
		this.object_map[objkey]=[cur,val];
		return true;}}
	    else if (contains(cur,val)) return false;
	    else {
	      cur.push(val); return true;}}
	  else  {
	    var cur=this.scalar_map[key];
	    if (!(cur)) {
	      this.scalar_map[key]=[val];
	      return true;}
	    else if (!(cur instanceof Array)) {
	      if (cur===val) return false;
	      else {
		this.scalar_map[key]=[cur,val];
		return true;}}
	    else if (contains(cur,val)) return false;
	    else {
	      cur.push(val); return true;}}};
	Map.prototype.drop=function(key,val) {
	  if (!(val)) {
	    if (isobject(key))
	      delete this.object_map
		[key.qid||key.oid||key.uuid||key._fdjtid||register(key)];
	    else delete this.scalar_map[key];}
	  else if (isobject(key)) {
	    var objkey=key.qid||key.oid||key.uuid||key._fdjtid||
	    register(key);
	    var cur=this.object_map[key];
	    if (!(cur)) return false;
	    else if (!(cur instanceof Array)) {
	      if (cur===val) {
		delete this.object_map[objkey];
		return true;}
	      else return false;}
	    else if ((pos=position(val,cur))>=0) {
	      if (cur.length===1) delete this.object_map[objkey];
	      else cur.splice(pos);
	      return true;}
	    else return false;}
	  else {
	    var cur=this.scalar_map[key]; var pos=-1;
	    if (!(cur)) return false;
	    else if (!(cur instanceof Array)) {
	      if (cur===val) {
		delete this.scalar_map[key];
		return true;}
	      else return false;}
	    else if ((pos=position(val,cur))>=0) {
	      if (cur.length===1)
		delete this.scalar_map[key];
	      else cur.splice(pos);
	      return true;}
	    else return false;}};
	fdjtKB.Map=Map;

	/* Indices */

	function Index() {
	    var scalar_indices={};
	    var object_indices={};
	    return function(item,prop,val,add){
	      var valkey; var indices=scalar_indices;
	      if (!(prop))
		return {scalars: scalar_indices, objects: object_indices};
	      else if (!(val))
		return {scalars: scalar_indices[prop],objects: object_indices[prop]};
	      else if (isobject(val)) {
		valkey=val.qid||val.uuid||val.oid||val._fdjtid||
		  register(val);
		indices=object_indices;}
	      else valkey=val;
	      var index=indices[prop];
	      if (!(item))
		if (!(index)) return [];
		else return Set(index[valkey]);
 	      var itemkey=
		((isobject(item))?
		 (item.qid||item.uuid||item.oid||
		  item._fdjtid||register(item)):
		 (item));
	      if (!(index))
		if (add) {
		  indices[prop]=index={};
		  index[valkey]=[itemkey];
		  return true;}
		else return false;
	      var curvals=index[valkey];
	      if (curvals) {
		var pos=position(curvals,itemkey);
		if (pos<0) {
		  if (add) {
		    curvals.push(itemkey);
		    return true;}
		  else return false;}
		else if (add) return false;
		else {
		  var sortlen=curvals._sortlen;
		  curvals.splice(pos,1);
		  if (pos<sortlen) curvals._sortlen--;
		  return true;}}
	      else if (add) {
		index[valkey]=Set(itemkey);
		return true;}
	      else return false;};}
	fdjtKB.Index=Index;

	/* Refs */

	function Ref(pool,qid) {
	    if (pool) this.pool=pool;
	    if (qid) this.qid=qid;
	    return this;}
	fdjtKB.Ref=Ref;
	Pool.prototype.cons=function(qid){return new Ref(this,qid);};

	Ref.prototype.load=function(){
	  if (this._init) return this;
	  else if (this.pool.storage) 
	    return this.pool.storage.load(this);
	  else return undefined;};
	Ref.prototype.get=function(prop){
	    if (this.hasOwnProperty(prop)) return this[prop];
	    else if (this.pool.storage) {
		var fetched=this.pool.storage.get(this,prop);
		if (typeof fetched !== 'undefined')
		    this[prop]=fetched;
		else if (this.hasOwnProperty(prop))
		    return this[prop];
		else return fetched;}
	    else return undefined;};
	Ref.prototype.getSet=function(prop){
	    if (this.hasOwnProperty(prop)) {
		var val=this[prop];
		if (val instanceof Array)
		    if (val._sortlen===val.length) return val;
		else return setify(val);
		else return [val];}
	    else if (this.pool.storage) {
		var fetched=this.pool.storage.get(this,prop);
		if (typeof fetched !== 'undefined')
		    this[prop]=fetched;
		return setify(fetched);}
	    else return [];};
	Ref.prototype.getArray=function(prop){
	    if (this.hasOwnProperty(prop)) {
		var val=this[prop];
		if (val instanceof Array) return val;
		else return [val];}
	    else if (this.pool.storage) {
		var fetched=this.pool.storage.get(this,prop);
		if (typeof fetched !== 'undefined')
		    this[prop]=fetched;
		return [fetched];}
	    else return [];};
	Ref.prototype.add=function(prop,val){
	    if (this.pool.xforms[prop])
		val=this.pool.xforms[prop](val)||val;
	    if (this.hasOwnProperty(prop)) {
		var cur=this[prop];
		if (cur===val) return false;
		else if (cur instanceof Array)
		    if (!(set_add(cur,val))) return false;
		else {}
		else this[prop]=Set([cur,val]);}
	    else this[prop]=val;
	    if (this.pool.storage)
		this.pool.storage.add(this,prop,val);
	    if ((this.pool.effects)&&(this.pool.effects[prop]))
		this.pool.effects[prop](this,prop,val);
	    if (this.pool.index)
		this.pool.index(this,prop,val,true);};
	Ref.prototype.drop=function(prop,val){
	    if (typeof val === 'undefined') val=this[prop];
	    if (this.pool.xforms[prop])
		val=this.pool.xforms[prop](val)||val;
	    var vals=false;
	    if (this.hasOwnProperty(prop)) {
		var cur=this[prop];
		if (cur===val) delete this[prop];
		else if (cur instanceof Array) {
		    if (!(set_drop(cur,val))) return false;
		    if (cur.length===0) delete this[prop];}
		else return false;
		if (this.pool.storage)
		    this.pool.storage.drop(this,prop,val);
		if (this.pool.index)
		    this.pool.index(this,prop,val,false);
		return true;}
	    else return false;};
	Ref.prototype.test=function(prop,val){
	    if (this.pool.xforms[prop])
		val=this.pool.xforms[prop](val)||val;
	    if (this.hasOwnProperty(prop)) {
		if (typeof val === 'undefined') return true;
		var cur=this[prop];
		if (cur===val) return true;
		else if (cur instanceof Array)
		    if (contains(cur,val)) return true;
		else return false;
		else return false;}
	    else if (this.pool.storage) {
		var fetched=this.pool.storage.get(this,prop);
		if (typeof fetched !== 'undefined')
		    this[prop]=fetched;
		else return false;
		if (typeof val === 'undefined') return true;
		else return this.test(prop,val);}
	    else return false;};
	Ref.prototype.ondrop=function(){
	    for (var prop in this)
		if ((prop!=='pool')&&(prop!=='qid'))
		    this.drop(prop,this[prop]);};
	function init_ref(data){
	  var pool=this.pool; var map=pool.map;
	  if ((this._init)&&(this._init>init_start)) return this;
	  if ((debug)&&(pool.traceref))
	    fdjtLog("Initial reference to %o <== %o",this,data);
	  for (key in data)
	    if (!((key==='qid')||(key==='pool'))) {
	      var value=data[key];
	      // Add ref aliases when unique
	      if ((key==='uuid')||(key==='oid')) {
		if (!(map[value])) map[value]=this;
		else if (map[value]!==this)
		  fdjtLog.warn("identifier conflict %o=%o for %o and %o",
			       key,value,map[value],this);
		else {}}
	      if (value instanceof Array) {
		var i=0; var len=value.length;
		while (i<len) this.add(key,value[i++]);}
	      else this.add(key,value);}
	  var inits=pool.inits;
	  if ((inits)&&(debug)&&(pool.traceinit))
	    fdjtLog("Running pool inits for %o: %o",this,inits);
	  var i=0; var lim;
	  this._init=fdjtTime();
	  if (inits) {
	    var lim=inits.length;
	    while (i<lim) inits[i++](this);}
	  var inits=this._inits; delete this._inits;
	  if ((inits)&&(debug)&&(pool.traceinit))
	    fdjtLog("Running delayed inits for %o: %o",this,inits);
	  if (inits) {
	      delete this._inits;
	      i=0; lim=inits.length;
	      while (i<lim) inits[i++](this);}
	  return this;}
	Ref.prototype.init=init_ref;
	// This isn't right
	Ref.prototype.update=init_ref;
	Ref.prototype.oninit=function(fcn,name){
	  var debugging=((debug)&&(this.pool.traceinit));
	    if (this._init) {
	      if (debugging) {
		if (name)
		  fdjtLog("Init (%s) %o on pre-existing %o",name,fcn,this);
		else fdjtLog("Init %o on pre-existing %o",fcn,this);}
	      fcn(this);
	      return true;}
	    else if (this._inits) {
	      // Save up the init functions
	      if (!(name)) {
		if (!(contains(this._inits,fcn))) {
		  if (debugging) fdjtLog("Delaying init on %o: %o",this,fcn);
		  this._inits.push(fcn);}}
	      // Don't do anything if the named init has already been added
	      else if (this._inits[name]) {
		/* Note that name can't be anything that an array object
		   might inherit (like 'length'). */ }
	      else {
		if (debugging)
		  fdjtLog("Delaying init %s on %o: %o",name,this,fcn);
		this._inits[name]=fcn;
		this._inits.push(fcn);}}
	    else if (name) {
	      if (debugging)
		fdjtLog("Delaying init %s on %o: %o",name,this,fcn);
	      this._inits=[fcn];
	      this._inits[name]=fcn;}
	    else {
	      fdjtLog("Delaying init on %o: %o",this,fcn);
	      this._inits=[fcn];}
	  return false;};

	/* Using offline storage to back up pools
	   In the simplest model, the QID is just used as a key
	   in local storage to store a JSON version of the object. */

	function OfflineKB(pool){
	    this.pool=pool;
	    return this;}
	function offline_get(obj,prop){
	    var qid=obj.qid||obj.uuid||obj.oid;
	    var data=fdjtState.getLocal(qid);
	    if (data) obj.init(data);
	    return obj[prop];}
	OfflineKB.prototype.load=function(obj){
	  var qid=obj.qid||obj.uuid||obj.oid;
	  var data=fdjtState.getLocal(qid,true);
	  if (data) return obj.init(data);
	  else return undefined;};
	OfflineKB.prototype.get=offline_get;
	OfflineKB.prototype.add=function(obj,slotid,val){
	    var qid=obj.qid||obj.uuid||obj.oid;
	    if ((slotid)&&(val))
		fdjtState.setLocal(qid,JSON.stringify(obj));};
	OfflineKB.prototype.drop=function(obj,slotid,val){
	    var qid=obj.qid||obj.uuid||obj.oid;
	    if (!(slotid)) fdjtState.dropLocal(qid);
	    else fdjtState.setLocal(qid,JSON.stringify(obj));};
	OfflineKB.prototype.Import=function(obj){
	    var qid=obj.qid||obj.uuid||obj.oid;
	    fdjtState.setLocal(qid,obj,true);};
	fdjtKB.OfflineKB=OfflineKB;
	
	/* Miscellaneous array and table functions */

	fdjtKB.add=function(obj,field,val,nodup){
	    if (arguments.length===2)
		return set_add(obj,field);
	    else if (obj instanceof Ref)
		return obj.add.apply(obj,arguments);
	    else if (nodup) 
		if (obj.hasOwnProperty(field)) {
		    var vals=obj[field];
		    if (!(contains(vals,val)))
			obj[field].push(val);
		    else {}}
	    else obj[field]=new Array(val);
	    else if (obj.hasOwnProperty(field))
		obj[field].push(val);
	    else obj[field]=new Array(val);
	    if ((obj._all) && (!(contains(obj._all,field))))
		obj._all.push(field);};

	fdjtKB.drop=function(obj,field,val){
	    if (arguments.length===2)
		return set_drop(obj,field);
	    else if (obj instanceof Ref)
		return obj.drop.apply(obj,arguments);
	    else if (!(val))
		/* Drop all vals */
		obj[field]=new Array();
	    else if (obj.hasOwnProperty(field)) {
		var vals=obj[field];
		var pos=position(vals,val);
		if (pos<0) return;
		else vals.splice(pos,1);}
	    else {}};

	fdjtKB.test=function(obj,field,val){
	    if (arguments.length===2)
		return set_contains(obj,field);
	    else if (obj instanceof Ref)
		return obj.test.apply(obj,arguments);
	    else if (typeof val === "undefined")
		return (((obj.hasOwnProperty) ?
			 (obj.hasOwnProperty(field)) : (obj[field])) &&
			((obj[field].length)>0));
	    else if (obj.hasOwnProperty(field)) 
		if (position(obj[field],val)<0)
		    return false;
	    else return true;
	    else return false;};

	fdjtKB.insert=function(array,value){
	    if (position(array,value)<0) array.push(value);};

	fdjtKB.remove=function(array,value,count){
	    var pos=position(array,value);
	    if (pos<0) return array;
	    array.splice(pos,1);
	    if (count) {
		count--;
		while ((count>0) &&
		       ((pos=position(array,value,pos))>=0)) {
		    array.splice(pos,1); count--;}}
	    return array;};

	fdjtKB.indexof=function(array,elt,pos){
	    if (array.indexOf)
		if (pos)
		    return array.indexOf(elt,pos);
	    else return array.indexOf(elt);
	    else {
		var i=pos||0;
		while (i<array.length)
		    if (array[i]===elt) return i;
		else i++;
		return -1;}};

	fdjtKB.contains=function(array,elt){
	    if (array.indexOf)
		return (array.indexOf(elt)>=0);
	    else {
		var i=0; var len=array.length;
		while (i<len)
		    if (array[i]===elt) return true;
		else i++;
		return false;}};
	
	return fdjtKB;})();

/* Emacs local variables
;;;  Local variables: ***
;;;  compile-command: "make; if test -f ../makefile; then cd ..; make; fi" ***
;;;  End: ***
*/
/* -*- Mode: Javascript; -*- */

var fdjt_state_id="$Id$";
var fdjt_state_version=parseInt("$Revision$".slice(10,-1));
fdjt_versions.decl("fdjt/state",fdjt_state_version);
fdjt_versions.decl("fdjt",fdjt_state_version);

/* Copyright (C) 2009-2011 beingmeta, inc.
   This file is a part of the FDJT web toolkit (www.fdjt.org)
   This file provides extended Javascript utility functions
   of various kinds.

   This program comes with absolutely NO WARRANTY, including implied
   warranties of merchantability or fitness for any particular
   purpose.

   Use, modification, and redistribution of this program is permitted
   under either the GNU General Public License (GPL) Version 2 (or
   any later version) or under the GNU Lesser General Public License
   (version 3 or later).

   These licenses may be found at www.gnu.org, particularly:
   http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
   http://www.gnu.org/licenses/lgpl-3.0-standalone.html

*/

var fdjtState=
  (function(){

    function fdjtState(name,val,persist){
      if (arguments.length===1)
	return ((window.sessionStorage)&&(getSession(name)))||
	  ((window.sessionStorage)&&(getLocal(name)))||
	  getCookie(name);
      else if (persist)
	if (window.localStorage)
	  if (val) setLocal(name,val);
	  else dropLocal(name);
	else {
	  var domain=fdjtState.domain||location.hostname;
	  var path=fdjtState.path||"/";
	  var duration=fdjtState.duration||(3600*24*365*7);
	  if (val) setCookie(name,val,duration,path,domain);
	  else clearCookie(name,path,domain);}
      else if (val)
	if (window.sessionStorage) setSession(name,val);
	else setCookie(name,val);
      else if (window.sessionStorage) dropSession(name);
      else clearCookie(name);};
    fdjtState.domain=false;
    fdjtState.path=false;
    fdjtState.duration=false;

    /* Old-school cookies */

    function getCookie(name,parse){
      try {
	var cookies=document.cookie;
	var namepat=new RegExp("(^|(; ))"+name+"=");
	var pos=cookies.search(namepat);
	var valuestring;
	if (pos>=0) {
	  var start=cookies.indexOf('=',pos)+1;
	  var end=cookies.indexOf(';',start);
	  if (end>0) valuestring=cookies.slice(start,end);
	  else valuestring=cookies.slice(start);}
	else return false;
	if (parse)
	  return JSON.parse(decodeURIComponent(valuestring));
	else return decodeURIComponent(valuestring);}
      catch (ex) {
	return false;}}
    fdjtState.getCookie=getCookie;

    function setCookie(name,value,expires,path,domain){
      try {
	if (value) {
	  var valuestring=
	    ((typeof value === 'string') ? (value) :
	     (value.toJSON) ? (value.toJSON()) :
	     (value.toString) ? (value.toString()) : (value));
	  var cookietext=name+"="+encodeURIComponent(valuestring);
	  if (expires)
	    if (typeof(expires)==='string')
	      cookietext=cookietext+'; '+expires;
	    else if (expires.toGMTString)
	      cookietext=cookietext+"; expires="+expires.toGMTString();
	    else if (typeof(expires)==='number')
	      if (expires>0) {
		var now=new Date();
		now.setTime(now.getTime()+expires);
		cookietext=cookietext+"; expires="+now.toGMTString;}
	      else cookietext=cookietext+"; expires=Sun 1 Jan 2000 00:00:00 UTC";
	    else {}
	  if (path) cookietext=cookietext+"; path="+path;
	  // This certainly doesn't work generally and might not work ever
	  if (domain) cookietext=cookietext+"; domain="+domain;
	  // fdjtTrace("Setting cookie %o cookietext=%o",name,cookietext);
	  document.cookie=cookietext;}
	else clearCookie(name,path,domain);}
      catch (ex) {
	fdjtLog.warn("Error setting cookie %s",name);}}
    fdjtState.setCookie=setCookie;
    
    function clearCookie(name,path,domain){
      try {
	var valuestring="ignoreme";
	var cookietext=name+"="+encodeURIComponent(valuestring)+
	  "; expires=Sun 1 Jan 2000 00:00:00 UTC";
	if (path) cookietext=cookietext+"; path="+path;
	// This certainly doesn't work generally and might not work ever
	if (domain) cookietext=cookietext+"; domain="+domain;
	// fdjtTrace("Clearing cookie %o: text=%o",name,cookietext);
	document.cookie=cookietext;}
      catch (ex) {
	fdjtLog.warn("Error clearing cookie %s",name);}}
    fdjtState.clearCookie=clearCookie;

    /* Session storage */

    function setSession(name,val,unparse){
      if (unparse) val=JSON.stringify(val);
      if (window.sessionStorage)
	window.sessionStorage[name]=val;
      else setCookie(name,val);}
    fdjtState.setSession=setSession;

    function getSession(name,parse){
      var val=((window.sessionStorage)?
	       (window.sessionStorage[name]):
	       (fdjtGetCookie(name)));
      if (val)
	if (parse) return JSON.parse(val); else return val;
      else return false;}
    fdjtState.getSession=getSession;

    function dropSession(name){
      if (window.sessionStorage)
	return window.sessionStorage.removeItem(name);
      else clearCookie(name);}
    fdjtState.dropSession=dropSession;

    /* Local storage (persists between sessions) */

    function setLocal(name,val,unparse){
      if (!(name)) throw { error: "bad name",name: name};
      if (unparse) val=JSON.stringify(val);
      if (window.localStorage)
	window.localStorage[name]=val;}
    fdjtState.setLocal=setLocal;

    function getLocal(name,parse){
      if (window.localStorage) {
	var val=window.localStorage[name];
	if (val)
	  if (parse) return JSON.parse(val); else return val;
	else return false;}
      else return false;}
    fdjtState.getLocal=getLocal;

    function dropLocal(name){
      if (window.localStorage)
	return window.localStorage.removeItem(name);
      else return false;}
    fdjtState.dropLocal=dropLocal;
    
    function clearLocal(){
      if (window.localStorage) {
	var storage=window.localStorage;
	var i=0; var lim=storage.length;
	var keys=[];
	while (i<lim) keys.push(storage.key(i++));
	i=0; while (i<lim) storage.removeItem(keys[i++]);}}
    fdjtState.clearLocal=clearLocal;

    /* Gets arguments from the query string */
    function getQuery(name,multiple,matchcase,verbatim){
      if (!(location.search))
	if (multiple) return [];
	else return false;
      var results=[];
      var ename=encodeURIComponent(name);
      var namepat=new RegExp
	("(&|^|\\?)"+ename+"(=|&|$)",((matchcase)?"g":"gi"));
      var query=location.search;
      var start=query.search(namepat);
      while (start>=0) {
	// Skip over separator if non-initial
	if ((query[start]==='?')||(query[start]==='&')) start++;
	// Skip over the name
	var valstart=start+ename.length; var end=false;
	if (query[valstart]==="=") {
	  var valstring=query.slice(valstart+1);
	  end=valstring.search(/(&|$)/g);
	  if (end<=0) {
	    results.push("");
	    if (!(multiple)) break;}
	  else {
	    results.push(valstring.slice(0,end));
	    if (!(multiple)) break;}}
	else if (multiple)
	  results.push(query.slice(start,end));
	else if (verbatim)
	  return query.slice(start,end);
	else return querydecode(query.slice(start,end));
	if (end>0) {
	  query=query.slice(end);
	  start=query.search(namepat);}}
      if (!(verbatim)) {
	var i=0; var lim=results.length;
	while (i<lim) {results[i]=querydecode(results[i]); i++;}}
      if (multiple) return results;
      else if (results.length)
	return results[0];
      else return false;}
    fdjtState.getQuery=getQuery;
    
    function querydecode(string){
      if (decodeURIComponent)
	return decodeURIComponent(string);
      else return 
	     string.replace
	     (/%3A/gi,":").replace
	     (/%2F/gi,"/").replace
	     (/%3F/gi,"?").replace
	     (/%3D/gi,"=").replace
	     (/%20/gi," ").replace
	     (/%40/gi,"@").replace
	     (/%23/gi,"#");}

    function test_opt(pos,neg){
      var pospat=((pos)&&(new RegExp("\\b"+pos+"\\b")));
      var negpat=((neg)&&negative_opt_pat(neg));
      var i=2; while (i<arguments.length) {
	var arg=arguments[i++];
	if (!(arg)) continue;
	else if (typeof arg === 'string')
	  if ((pospat)&&(arg.search(pospat)>=0)) return true;
	  else if ((negpat)&&(arg.search(negpat)>=0)) return false;
	  else continue;
	else if (arg.length) {
	  var j=0; var len=arg.length;
	  while (j<len)
	    if ((pos)&&(arg[j]===pos)) return true;
	    else if ((neg)&&(arg[j]===neg)) return false;
	    else j++;
	  return false;}
	else continue;}
      return false;}
    fdjtState.testOption=test_opt;

    function negative_opt_pat(neg){
      if (!(neg)) return neg;
      else if (typeof neg === 'string')
	return (new RegExp("\\b"+neg+"\\b","gi"));
      else if (neg.length) {
	var rule="\\b(";
	var i=0; while (i<neg.length) {
	  var name=neg[i];
	  if (i>0) rule=rule+"|";
	  rule=rule+"("+name+")";
	  i++;}
	rule=rule+")\\b";
	return new RegExp(rule,"gi");}
      else return false;}

    fdjtState.argVec=function(argobj,start){
      var i=start||0;
      var result=new Array(argobj.length-i);
      while (i<argobj.length) {
	result[i-start]=argobj[i]; i++;}
      return result;};

    var zeros="000000000000000000000000000000000000000000000000000000000000000";
    function zeropad(string,len){
      if (string.length===len) return string;
      else if (string.length>len) return string.slice(0,len);
      else return zeros.slice(0,len-string.length)+string;}

    var nodeid=
      zeropad(((Math.floor(Math.random()*65536)).toString(16)+
	       (Math.floor(Math.random()*65536)).toString(16)+
	       (Math.floor(Math.random()*65536)).toString(16)+
	       (Math.floor(Math.random()*65536)|0x01)).toString(16),
	      12);
    
    var default_version=17; 
    var clockid=Math.floor(Math.random()*16384); var msid=1;
    var last_time=new Date().getTime();
    
    fdjtState.getNodeID=function(){return nodeid;};
    fdjtState.setNodeID=function(arg){
      if (typeof arg==='number')
	nodeid=zeropad(arg.toString(16),12);
      else if (typeof arg === 'string')
	if (arg.search(/[^0123456789abcdefABCDEF]/)<0)
	  nodeid=zeropad(arg,12);
	else throw {error: 'invalid node id',value: arg};
      else throw {error: 'invalid node id',value: arg};};

    function getUUID(node){
      var now=new Date().getTime();
      if (now<last_time) {now=now*10000; clockid++;}
      else if (now===last_time)	now=now*10000+(msid++);
      else {now=now*10000; msid=1;}
      now=now+122192928000000000;
      if (!(node)) node=nodeid;
      var timestamp=now.toString(16); var tlen=timestamp.length;
      if (tlen<15) timestamp=zeros.slice(0,15-tlen)+timestamp;
      return timestamp.slice(7)+"-"+timestamp.slice(3,7)+
	"-1"+timestamp.slice(0,3)+
	"-"+(32768+(clockid%16384)).toString(16)+
	"-"+((node)?
	     ((typeof node === 'number')?
	      (zeropad(node.toString(16),12)):
	      (zeropad(node,12))):
	     (nodeid));}
    fdjtState.getUUID=getUUID;
    
    // Getting version information
    function versionInfo(){
      var s=navigator.appVersion; var result={};
      var start;
      while ((start=s.search(/\w+\/\d/g))>=0) {
	var slash=s.indexOf('/',start);
	var afterslash=s.slice(slash+1);
	var num_end=afterslash.search(/\W/);
	var numstring=afterslash.slice(0,num_end);
	try {
	  result[s.slice(start,slash)]=parseInt(numstring);}
	catch (ex) {
	  result[s.slice(start,slash)]=numstring;}
	s=afterslash.slice(num_end);}
      return result;}
    fdjtState.versionInfo=versionInfo;

    return fdjtState;})();

/* Emacs local variables
   ;;;  Local variables: ***
   ;;;  compile-command: "make; if test -f ../makefile; then cd ..; make; fi" ***
   ;;;  End: ***
*/
/* -*- Mode: Javascript; -*- */

var fdjt_log_id="$Id$";
var fdjt_log_version=parseInt("$Revision$".slice(10,-1));
fdjt_versions.decl("fdjt/log",fdjt_log_version);
fdjt_versions.decl("fdjt",fdjt_log_version);

/* Copyright (C) 2009-2011 beingmeta, inc.
   This file is a part of the FDJT web toolkit (www.fdjt.org)
   This file provides extended Javascript utility functions
   of various kinds.

   This program comes with absolutely NO WARRANTY, including implied
   warranties of merchantability or fitness for any particular
   purpose.

   Use, modification, and redistribution of this program is permitted
   under either the GNU General Public License (GPL) Version 2 (or
   any later version) or under the GNU Lesser General Public License
   (version 3 or later).

   These licenses may be found at www.gnu.org, particularly:
   http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
   http://www.gnu.org/licenses/lgpl-3.0-standalone.html

*/

var fdjtLog=(function(){
    var backlog=[];

    function fdjtLog(string){
	var output=false;
	if ((fdjtLog.doformat)&&(typeof fdjtString !== 'undefined'))
	    output=fdjtString.apply(null,arguments);
	if (fdjtLog.console_fn) {
	    if (output) fdjtLog.console_fn.call(fdjtLog.console,output);
	    else fdjtLog.console_fn.apply(fdjtLog.console,arguments);}
	if (fdjtLog.console) {
	    var domconsole=fdjtLog.console;
	    var entry=fdjtDOM("div.fdjtlog",fdjtDOM("span.time",fdjtET()));
	    if (output) fdjtDOM(entry,output);
	    else fdjtDOM(entry,fdjtString.apply(null,arguments));
	    if (typeof domconsole === 'string') {
		var found=document.getElementById(domconsole);
		if (found) {
		    domconsole=fdjtLog.console=found;
		    var i=0; var lim=backlog.length;
		    while (i<lim) fdjtDOM(domconsole,backlog[i++]);
		    backlog=[];}
		else domconsole=false;}
	    else if (!(domconsole.nodeType)) domconsole=false;
	    if (domconsole)
		fdjtDOM.append(domconsole,entry);
	    else backlog.push(entry);}
	if ((fdjtLog.useconsole)||
	    ((!(fdjtLog.console))&&(!(fdjtLog.console_fn))))
	    if ((window.console) && (window.console.log) &&
		(window.console.count)) {
		if (output)
		    window.console.log.call(
			window.console,"["+fdjtET()+"s] "+output);
		else {
		    var newargs=new Array(arguments.length+1);
		    newargs[0]="[%fs] "+string;
		    newargs[1]=fdjtET();
		    var i=1; var lim=arguments.length;
		    while (i<lim) {newargs[i+1]=arguments[i]; i++;}
		    window.console.log.apply(window.console,newargs);}}}
    fdjtLog.console=null;
    fdjtLog.id="$Id$";
    fdjtLog.version=parseInt("$Revision$".slice(10,-1));

    fdjtLog.warn=function(string){
	if ((!(fdjtLog.console_fn))&&
	    (!(window.console)&&(window.console.log)&&(window.console.log.count))) {
	    var output=fdjtString.apply(null,arguments);
	    alert(output);}
	else fdjtLog.apply(null,arguments);};

    fdjtLog.uhoh=function(string){
	if (fdjtLog.debugging) fdjtLog.warn.call(this,arguments);}

    fdjtLog.bkpt=function(string){
	var output=false;
	if ((fdjtLog.doformat)&&(typeof fdjtString !== 'undefined'))
	    output=fdjtString.apply(null,arguments);
	if (fdjtLog.console_fn)
	    if (output) fdjtLog.console_fn(fdjtLog.console,output);
	else fdjtLog.console_fn.apply(fdjtLog.console,arguments);
	else if ((window.console) && (window.console.log) &&
		 (window.console.count))
	    if (output)
		window.console.log.call(window.console,output);
	else window.console.log.apply(window.console,arguments);
    };

    fdjtLog.useconsole=true;

    return fdjtLog;})();

// This is for temporary trace statements; we use a different name
//  so that they're easy to find.
var fdjtTrace=fdjtLog;

/* Emacs local variables
   ;;;  Local variables: ***
   ;;;  compile-command: "make; if test -f ../makefile; then cd ..; make; fi" ***
   ;;;  End: ***
*/
/* -*- Mode: Javascript; -*- */

var fdjt_ui_id="$Id$";
var fdjt_ui_version=parseInt("$Revision$".slice(10,-1));
fdjt_versions.decl("fdjt/ui",fdjt_ui_version);
fdjt_versions.decl("fdjt",fdjt_ui_version);

/* Copyright (C) 2009-2011 beingmeta, inc.
   This file is a part of the FDJT web toolkit (www.fdjt.org)
   This file provides extended Javascript utility functions
   of various kinds.

   This program comes with absolutely NO WARRANTY, including implied
   warranties of merchantability or fitness for any particular
   purpose.

   Use, modification, and redistribution of this program is permitted
   under either the GNU General Public License (GPL) Version 2 (or
   any later version) or under the GNU Lesser General Public License
   (version 3 or later).

   These licenses may be found at www.gnu.org, particularly:
   http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
   http://www.gnu.org/licenses/lgpl-3.0-standalone.html

*/

var fdjtUI=
    {CoHi: {classname: "cohi"},AutoPrompt: {}, InputHelp: {},
     Expansion: {},Collapsible: {},
     Tabs: {}, MultiText: {}};

/* Co-highlighting */
/* When the mouse moves over a named element, the 'cohi' class is added to
   all elements with the same name. */
(function(){
    var highlights={};
    function highlight(namearg,classname_arg){
	var classname=((classname_arg) || (fdjtUI.CoHi.classname));
	var newname=(namearg.name)||(namearg);
	var cur=highlights[classname];
	if (cur===newname) return;
	if (cur) {
	    var drop=document.getElementsByName(cur);
	    var i=0, n=drop.length;
	    while (i<n) fdjtDOM.dropClass(drop[i++],classname);}
	highlights[classname]=newname||false;
	if (newname) {
	    var elts=document.getElementsByName(newname);
	    var n=elts.length, i=0;
	    while (i<n) fdjtDOM.addClass(elts[i++],classname);}}
    
    fdjtUI.CoHi.onmouseover=function(evt,classname_arg){
	var target=fdjtDOM.T(evt);
	while (target)
	    if ((target.tagName==='INPUT') || (target.tagName==='TEXTAREA') ||
		((target.tagName==='A') && (target.href)))
		return;
	else if (target.name) break;  
	else target=target.parentNode;
	if (!(target)) return;
	highlight(target.name,classname_arg);};
    fdjtUI.CoHi.onmouseout=function(evt,classname_arg){
	var target=fdjtDOM.T(evt);
	highlight(false,((classname_arg) || (fdjtUI.CoHi.classname)));};
})();

/* CheckSpans:
   Text regions which include a checkbox where clicking toggles the checkbox. */
(function(){
    var hasClass=fdjtDOM.hasClass;
    var addClass=fdjtDOM.addClass;
    var dropClass=fdjtDOM.dropClass;
    var toggleClass=fdjtDOM.toggleClass;
    var getParent=fdjtDOM.getParent;
    var getChildren=fdjtDOM.getChildren;

    function CheckSpan(spec,varname,val,checked){
	var input=fdjtDOM.Input('input[type=checkbox]',varname,val);
	var span=fdjtDOM(spec||"span.checkspan",input);
	if (checked) {
	    input.checked=true;
	    fdjtDOM.addClass(span,"ischecked");}
	else input.checked=false;
	return span;}
    fdjtUI.CheckSpan=CheckSpan;

    function checkspan_onclick(evt) {
	evt=evt||event;
	target=evt.target||evt.srcTarget;
	var checkspan=getParent(target,".checkspan");
	if ((target.tagName==='INPUT')&&
	    ((target.type=='checkbox')||(target.type=='radio'))) {
	    target.blur();
	    if (target.checked) addClass(checkspan,"checked");
	    else dropClass(checkspan,"checked");}
	else {
	    var inputs=getChildren
	    (checkspan,function(elt){
		return (elt.nodeType===1)&&
		    (elt.tagName==='INPUT')&&
		    ((elt.type=='checkbox')||(elt.type=='radio'));});
	    var input=((inputs)&&(inputs.length)&&(inputs[0]));
	    if (input) 
		if (input.checked) {
		    dropClass(checkspan,"ischecked");
		    input.checked=false; input.blur();}
	    else {
		addClass(checkspan,"ischecked");
		input.checked=true; input.blur();}
	    else toggleClass(checkspan,"ischecked");}}
    fdjtUI.CheckSpan.onclick=checkspan_onclick;

    function checkspan_set(checkspan,checked){
	if (!(hasClass(checkspan,".checkspan")))
	    checkspan=getParent(checkspan,".checkspan")||checkspan;
	var inputs=getChildren
	(checkspan,function(node){
	    return (node.tagName==='INPUT')&&
		((node.type=='checkbox')||(node.type=='radio'));});
	var input=((inputs)&&(inputs.length)&&(inputs[0]));
	if (checked) {
	    input.checked=true; addClass(checkspan,"ischecked");}
	else {
	    input.checked=false; dropClass(checkspan,"ischecked");}}
    fdjtUI.CheckSpan.set=checkspan_set;})();

(function(){

    var hasClass=fdjtDOM.hasClass;
    var addClass=fdjtDOM.addClass;
    var dropClass=fdjtDOM.dropClass;

    function show_help_onfocus(evt){
	var target=fdjtDOM.T(evt);
	while (target)
	    if ((target.nodeType==1) &&
		((target.tagName === 'INPUT') ||
		 (target.tagName === 'TEXTAREA')) &&
		(target.getAttribute('helptext'))) {
		var helptext=fdjtID(target.getAttribute('helptext'));
		if (helptext) fdjtDOM.addClass(helptext,"showhelp");
		return;}
	else target=target.parentNode;}
    function autoprompt_onfocus(evt){
	evt=evt||event||null;
	var elt=fdjtDOM.T(evt);
	if ((elt) && (hasClass(elt,'isempty'))) {
	    elt.value=''; dropClass(elt,'isempty');}
	show_help_onfocus(evt);}

    function hide_help_onblur(evt){
	var target=fdjtDOM.T(evt);
	while (target)
	    if ((target.nodeType==1) &&
		((target.tagName === 'INPUT') || (target.tagName === 'TEXTAREA')) &&
		(target.getAttribute('HELPTEXT'))) {
		var helptext=fdjtID(target.getAttribute('HELPTEXT'));
		if (helptext) dropClass(helptext,"showhelp");
		return;}
	else target=target.parentNode;}
    function autoprompt_onblur(evt){
	var elt=fdjtDOM.T(evt);
	if (elt.value==='') {
	    addClass(elt,'isempty');
	    var prompt=(elt.prompt)||(elt.getAttribute('prompt'))||(elt.title);
	    if (prompt) elt.value=prompt;}
	else dropClass(elt,'isempty');
	hide_help_onblur(evt);}
    
    // Removes autoprompt text from empty fields
    function autoprompt_cleanup(form) {
	var elements=fdjtDOM.getChildren(form,".isempty");
	if (elements) {
	    var i=0; var lim=elements.length;
	    while (i<elements.length) elements[i++].value="";}}
    function autoprompt_onsubmit(evt) {
	var form=fdjtDOM.T(evt);
	autoprompt_cleanup(form);}

    var isEmpty=fdjtString.isEmpty;
    // Adds autoprompt handlers to autoprompt classes
    function autoprompt_setup(arg,nohandlers) {
	var forms=
	    ((arg.tagName==="FORM")?[arg]:
	     (fdjtDOM.getChildren(arg||document.body,"FORM")));
	var i=0; var lim=forms.length;
	while (i<lim) {
	    var form=forms[i++];
	    var inputs=fdjtDOM.getChildren
	    (form,"INPUT.autoprompt,TEXTAREA.autoprompt");
	    if (inputs.length) {
		var j=0; var jlim=inputs.length;
		while (j<jlim) {
		    var input=inputs[j++];
		    input.blur();
		    if (isEmpty(input.value)) {
			addClass(input,"isempty");
			var prompt=(input.prompt)||
			    (input.getAttribute('prompt'))||(input.title);
			if (prompt) input.value=prompt;}
		    if (!(nohandlers)) {
			fdjtDOM.addListener(input,"focus",autoprompt_onfocus);
			fdjtDOM.addListener(input,"blur",autoprompt_onblur);}}
		if (!(nohandlers))
		    fdjtDOM.addListener(form,"submit",autoprompt_onsubmit);}}}
    
    fdjtUI.AutoPrompt.setup=autoprompt_setup;
    fdjtUI.AutoPrompt.onfocus=autoprompt_onfocus;
    fdjtUI.AutoPrompt.onblur=autoprompt_onblur;
    fdjtUI.AutoPrompt.onsubmit=autoprompt_onsubmit;
    fdjtUI.AutoPrompt.cleanup=autoprompt_cleanup;
    fdjtUI.InputHelp.onfocus=show_help_onfocus;
    fdjtUI.InputHelp.onblur=hide_help_onblur;})();


(function(){
  function multitext_keypress(evt,sepch){
	evt=(evt)||(event);
	var ch=evt.charCode;
	var target=fdjtUI.T(evt);
	if (typeof sepch === 'string') sepch=sepch.charCodeAt(0);
	if ((ch!==13)&&(sepch)&&(sepch!=ch)) return;
	fdjtUI.cancel(evt);
	var checkbox=
	    fdjtDOM.Input("[type=checkbox]",target.name,target.value);
	var div=fdjtDOM("div.checkspan",checkbox,target.value);
	checkbox.checked=true;
	fdjtDOM(target.parentNode,div);
	target.value='';}
    fdjtUI.MultiText.keypress=multitext_keypress;})();

(function(){
    var serial=0;

    /* Constants */
    // Always set to distinguish no options from false
    var FDJT_COMPLETE_OPTIONS=1;
    // Whether the completion element is a cloud (made of spans)
    var FDJT_COMPLETE_CLOUD=2;
    // Whether to require that completion match an initial segment
    var FDJT_COMPLETE_ANYWORD=4;
    // Whether to match case in keys to completions
    var FDJT_COMPLETE_MATCHCASE=8;
    // Whether to an enter character always picks a completion
    var FDJT_COMPLETE_EAGER=16;
    // Whether the key fields may contain disjoins (e.g. (dog|friend))
    // to be accomodated in matching
    var FDJT_COMPLETE_DISJOINS=32;
    // Default options
    var default_options=FDJT_COMPLETE_OPTIONS;
    // Max number of completions to show
    var maxcomplete=50;
    // Milliseconds to wait for auto complete
    var complete_delay=100;

    var hasClass=fdjtDOM.hasClass;
    var addClass=fdjtDOM.addClass;
    var dropClass=fdjtDOM.dropClass;
    var getChildren=fdjtDOM.getChildren;
    var getParent=fdjtDOM.getParent;
    var position=fdjtKB.position;

    var isEmpty=fdjtString.isEmpty;
    var hasPrefix=fdjtString.hasPrefix;
    var prefixAdd=fdjtString.prefixAdd;
    var prefixFind=fdjtString.prefixFind;
    var commonPrefix=fdjtString.commonPrefix;

    fdjtUI.FDJT_COMPLETE_OPTIONS=FDJT_COMPLETE_OPTIONS;
    fdjtUI.FDJT_COMPLETE_CLOUD=FDJT_COMPLETE_CLOUD;
    fdjtUI.FDJT_COMPLETE_ANYWORD=FDJT_COMPLETE_ANYWORD;
    fdjtUI.FDJT_COMPLETE_MATCHCASE=FDJT_COMPLETE_MATCHCASE;
    fdjtUI.FDJT_COMPLETE_EAGER=FDJT_COMPLETE_EAGER;

    function Completions(dom,input,options) {
	this.dom=dom||false; this.input=input||false;
	this.options=options||default_options;
	this.nodes=[]; this.values=[]; this.serial=++serial;
	this.cues=[]; this.displayed=[];
	this.prefixtree={strings: []};
	this.bykey={}; this.byvalue=new fdjtKB.Map();
	if (!(options&FDJT_COMPLETE_MATCHCASE)) this.stringmap={};
	this.initialized=false;
	return this;}
    Completions.probe=function(arg){
	if (arg.tagName==='INPUT') {
	    var cid=arg.getAttribute('COMPLETIONS');
	    arg=fdjtID(cid);
	    if (arg) completions.get(arg);
	    else return false;}
	else return completions.get(arg);};

    function getKey(node){
	return node.key||(node.getAttribute("key"))||(node.value)||
	    (node.getAttribute("value"))||
	    ((hasClass(node,"variation"))&&(fdjtDOM.textify(node)))||
	    ((hasClass(node,"completion"))&&(completionText(node,"")));}
    Completions.getKey=getKey;
    function completionText(node,sofar){
	if (hasClass(node,"variation")) return sofar;
	else if (node.nodeType===3) return sofar+node.nodeValue;
	else if ((node.nodeType===1)&&(node.childNodes)) {
	    var children=node.childNodes;
	    var i=0; var lim=children.length;
	    while (i<lim) {
		var child=children[i++];
		if (child.nodeType===3) sofar=sofar+child.nodeValue;
		else if (child.nodeType===1)
		    sofar=completionText(child,sofar);
		else {}}
	    return sofar;}
	else return sofar;}

    function getValue(node){
	if (!(hasClass(node,"completions")))
	    node=getParent(node,".completions");
	var completions=((node)&&(Completions.probe(node)));
	if (completions)
	    return completions.getValue(node);
	else return false;}
    Completions.getValue=getValue;

    function addNodeKey(node,keystring,ptree,bykey,anywhere){
	var keys=((anywhere)?(keystring.split(/\W/g)):[]).concat(keystring);
	var i=0; var lim=keys.length;
	while (i<lim) {
	    var key=keys[i++];
	    prefixAdd(ptree,key,0);
	    if ((bykey[key])&&(bykey.hasOwnProperty(key)))
		bykey[key].push(node);
	    else bykey[key]=new Array(node);
	    bykey._count++;}}

    function getNodes(string,ptree,bykey,matchcase){
	var result=[]; var direct=[]; var variations=[];
	var keystring=stdspace(string);
	if (isEmpty(keystring)) return [];
	if (!(matchcase)) keystring=string.toLowerCase();
	var strings=prefixFind(ptree,keystring,0);
	var prefix=false;
	var exact=[]; var exactheads=[]; var keys=[];
	var i=0; var lim=strings.length;
	while (i<lim) {
	    var string=strings[i++];
	    var isexact=(string===keystring);
	    if (prefix) prefix=commonPrefix(prefix,string);
	    else prefix=string;
	    var completions=bykey[string];
	    if (completions) {
		var j=0; var jlim=completions.length;
		while (j<jlim) {
		    var c=completions[j++];
		    if (hasClass(c,"completion")) {
			if (isexact) {exactheads.push(c); exact.push(c);}
			result.push(c); keys.push(string); direct.push(c);}
		    else {
			var head=getParent(c,".completion");
			if (head) {
			    if (isexact) exact.push(head);
			    result.push(head); keys.push(string);
			    variations.push(c);}}}}}
	if (exact.length) result.exact=exact;
	if (exactheads.length) result.exactheads=exactheads;
	result.prefix=prefix;
	result.strings=strings;
	result.matches=direct.concat(variations);
	return result;}

    function addCompletion(c,completion,key,value) {
	if (!(key)) key=completion.key||getKey(completion);
	if (!(value))
	    value=(completion.value)||(completion.getAttribute('value'))||key;
	var pos=position(c.nodes,completion);
	if (pos<0) {
	    c.nodes.push(completion); c.values.push(value);
	    c.byvalue.add(value,completion);}
	else return;
	var opts=c.options;
	var container=c.dom;
	var ptree=c.prefixtree;
	var bykey=c.bykey;
	var smap=c.stringmap;
	var stdkey=stdspace(key);
	var matchcase=(opts&FDJT_COMPLETE_MATCHCASE);
	var anyword=(opts&FDJT_COMPLETE_ANYWORD);
	if (!(matchcase)) {
	    var lower=stdkey.toLowerCase();
	    smap[lower]=stdkey;
	    stdkey=lower;}
	if (!(getParent(completion,container)))
	    fdjtDOM.append(container,completion," ");
	addNodeKey(completion,stdkey,ptree,bykey,anyword);
	if (hasClass(completion,"cue")) c.cues.push(completion);
	var variations=getChildren(completion,".variation");
	var i=0; var lim=variations.length;
	while (i<lim) {
	    var variation=variations[i++];
	    var vkey=stdspace(variation.key||getKey(variation));
	    addNodeKey(variation,vkey,ptree,bykey,anyword);}}

    function initCompletions(c){
	var completions=getChildren(c.dom,".completion");
	var i=0; var lim=completions.length;
	while (i<lim) addCompletion(c,completions[i++]);
	c.initialized=true;}

    Completions.prototype.addCompletion=function(completion) {
	if (!(this.initialized)) initCompletions(this);
	addCompletion(this,completion);};

    function updateDisplay(c,todisplay){
	var displayed=c.displayed;
	if (displayed) {
	    var i=0; var lim=displayed.length;
	    while (i<lim) dropClass(displayed[i++],"displayed");
	    c.displayed=displayed=[];}
	else c.displayed=displayed=[];
	if (todisplay) {
	    var i=0; var lim=todisplay.length;
	    while (i<lim) {
		var node=todisplay[i++];
		if (hasClass(node,"completion")) {
		    addClass(node,"displayed");
		    displayed.push(node);}
		else {
		    var head=getParent(node,".completion");
		    if ((head)&&(!(hasClass(head,"displayed")))) {
			displayed.push(node); displayed.push(head);
			addClass(head,"displayed");
			addClass(node,"displayed");}}}}}


    Completions.prototype.getCompletions=function(string) {
	if ((string===this.curstring)||(string===this.maxstring)||
	    ((this.curstring)&&(this.maxstring)&&
	     (hasPrefix(string,this.curstring))&&
	     (hasPrefix(this.maxstring,string))))
	    return this.result;
	else {
	    var result;
	    if (!(this.initialized)) initCompletions(this);
	    if (isEmpty(string)) {
		result=[]; result.prefix=""; result.matches=[];
		if (this.dom) addClass(this.dom,"noinput");}
	    else {
		result=getNodes(string,this.prefixtree,this.bykey);
		if (this.dom) dropClass(this.dom,"noinput");
		updateDisplay(this,result.matches);}
	    if ((this.stringmap)&&(this.strings)) {
		var stringmap=this.stringmap;
		var strings=this.strings;
		var i=0; var lim=strings.length;
		while (i<lim) {
		    var s=strings[i]; var m=stringmap[s];
		    if (m) strings[i++]=m;
		    else i++;}}
	    this.curstring=string;
	    this.maxstring=result.prefix||string;
	    this.result=result;
	    return result;}};

    Completions.prototype.getValue=function(completion) {
	if (completion.value) return completion.value;
	else if (completion.getAttribute("value"))
	    return completion.getAttribute("value");
	var pos=position(this.nodes,completion);
	if (pos<0) return false;
	else return this.values[pos];};
    Completions.prototype.getKey=function(completion) {
	if (completion.key) return completion.value;
	else if (completion.getAttribute("key"))
	    return completion.getAttribute("key");
	var pos=position(this.nodes,completion);
	if (pos<0) return false;
	else return this.values[pos];};

    Completions.prototype.complete=function(string){
	if (!(this.initialized)) initCompletions(this);
	// fdjtLog("Completing on %o",string);
	if ((!(string))&&(string!==""))
	    string=((this.getText)?(this.getText(this.input)):
		    (hasClass(this.input,"isempty"))?(""):
		    (this.input.value));
	if (isEmpty(string)) {
	    if (this.displayed) updateDisplay(this,false);
	    addClass(this.dom,"noinput");
	    dropClass(this.dom,"noresults");
	    return [];}
	var result=this.getCompletions(string);
	if ((!(result))||(result.length===0)) {
	    updateDisplay(this,false);
	    dropClass(this.dom,"noinput");
	    addClass(this.dom,"noresults");
	    return [];}
	else {
	    updateDisplay(this,result.matches);
	    dropClass(this.dom,"noinput");
	    dropClass(this.dom,"noresults");}
	return result;};

    Completions.prototype.getByValue=function(values,spec){
	if (!(this.initialized)) initCompletions(this);
	var result=[];
	var byvalue=this.byvalue;
	if (spec) spec=new fdjtDOM.Selector(spec);
	if (!(values instanceof Array)) values=[values];
	var i=0; var lim=values.length;
	while (i<lim) {
	    var value=values[i++];
	    var completions=byvalue.get(value);
	    if (completions) {
		if (spec) {
		    var j=0; var jlim=completions.length;
		    while (j<jlim) {
			if (spec.match(completions[j]))
			    result.push(completions[j++]);
			else j++;}}
		else result=result.concat(completions);}}
	return result;};
    Completions.prototype.getByKey=function(keys,spec){
	if (!(this.initialized)) initCompletions(this);
	var result=[];
	var byvalue=this.bykey;
	if (spec) spec=new fdjtDOM.Selector(spec);
	if (!(keys instanceof Array)) keys=[keys];
	var i=0; var lim=keys.length;
	while (i<lim) {
	    var key=keys[i++];
	    var completions=bykey.get(key);
	    if (completions) {
		if (spec) {
		    var j=0; var jlim=completions.length;
		    while (j<jlim) {
			if (spec.match(completions[j]))
			    result.push(completions[j++]);
			else j++;}}
		else result=result.concat(completions);}}
	return result;};

    Completions.prototype.setCues=function(values){
	if (!(this.initialized)) initCompletions(this);
	var cues=[];
	var byvalue=this.byvalue;
	var i=0; var lim=values.length;
	while (i<lim) {
	    var value=values[i++];
	    var completions=byvalue.get(value);
	    if (completions) {
		var j=0; var jlim=completions.length;
		while (j<jlim) {
		    var c=completions[j++];
		    if (hasClass(c,"cue")) continue;
		    addClass(c,"cue");
		    cues.push(c);}}}
	return cues;};
    
    Completions.prototype.docomplete=function(input,callback){
	if (!(this.initialized)) initCompletions(this);
	if (!(input)) input=this.input;
	var delay=this.complete_delay||complete_delay;
	var that=this;
	if (this.timer) {
	    clearTimeout(that.timer);
	    that.timer=false;}
	this.timer=setTimeout(
	    function(){
		if (!(input)) input=that.input;
		var completions=that.complete(input.value);
		if (callback) callback(completions);},
	    delay);}

    function stdspace(string){
	return string.replace(/\s+/," ").replace(/(^\s)|(\s$)/,"");}

    fdjtUI.Completions=Completions;

    var cached_completions={};

    var default_options=
	FDJT_COMPLETE_OPTIONS|
	FDJT_COMPLETE_CLOUD|
	FDJT_COMPLETE_ANYWORD;

    function onkey(evt){
	evt=evt||event;
	var target=fdjtUI.T(evt);
	var name=target.name;
	var completions=cached_completions[name];
	var compid=fdjtDOM.getAttrib(target,"completions");
	var dom=((compid)&&(fdjtID(compid)));
	if (!(dom)) return;
	if (!((completions)&&(completions.dom===dom))) {
	    completions=new Completions(dom,target,default_options);
	    cached_completions[name]=completions;}
	if (!(completions)) return;
	completions.docomplete();}
    fdjtUI.Completions.onkey=onkey;}());

/* Tabs */

(function(){
    var hasClass=fdjtDOM.hasClass;
    var addClass=fdjtDOM.addClass;
    var dropClass=fdjtDOM.dropClass;
    
    function tab_onclick(evt,shownclass){
	var elt=fdjtUI.T(evt);
	if (!(shownclass)) {
	    shownclass=
		fdjtDOM.findAttrib(elt,"shownclass","http://fdjt.org/")||
		"fdjtshown";}
	if (elt) {
	    var content_id=false;
	    while (elt.parentNode) {
		if (content_id=fdjtDOM.getAttrib(elt,"contentid")) break;
		else elt=elt.parentNode;}
	    if (!(content_id)) return;
	    var content=document.getElementById(content_id);
	    var parent=fdjtDOM.getParent(elt,".tabs")||elt.parentNode;
	    var sibs=fdjtDOM.getChildren(parent,".tab")||parent.childNodes;
	    if (content===null) {
		fdjtLog("No content for "+content_id);
		return;}
	    var i=0; while (i<sibs.length) {
		var node=sibs[i++]; var cid;
		if ((node.nodeType===1) &&
		    (cid=fdjtDOM.getAttrib(node,"contentid"))) {
		    if (!(cid)) continue;
		    var cdoc=document.getElementById(cid);
		    if (node===elt) {}
		    else if (hasClass(node,shownclass)) {
			dropClass(node,shownclass);
			if (cdoc) dropClass(cdoc,shownclass);}}}
	    if (hasClass(elt,shownclass)) {
		dropClass(elt,shownclass);
		dropClass(content,shownclass);}
	    else {
		addClass(elt,shownclass);
		addClass(content,shownclass);}
	    var tabstate=fdjtDOM.findAttrib(elt,'tabstate');
	    if (!(tabstate)) {}
	    else if (tabstate==='#') {
		var scrollstate={};
		fdjtUI.scrollSave(scrollstate);
		document.location.hash=tabstate+content_id;
		fdjtUI.scrollRestore(scrollstate);}
	    else fdjtState.setCookie(tabstate,content_id);
	    // This lets forms pass tab information along
	    return false;}}
    fdjtUI.Tabs.click=tab_onclick;
    
    function select_tab(tabbar,contentid,shownclass){
	if (!(shownclass)) {
	    shownclass=
		fdjtDOM.findAttrib(tabbar,"shownclass","http://fdjt.org/")||
		"fdjtshown";}
	var tabseen=false;
	var tabs=fdjtDOM.getChildren(tabbar,".tab");
	var i=0; while (i<tabs.length) {
	    var tab=tabs[i++];
	    if ((tab.getAttribute("contentid"))===contentid) {
		addClass(tab,shownclass); tabseen=true;}
	    else if (hasClass(tab,shownclass)) {
		dropClass(tab,shownclass);
		var cid=fdjtDOM.getAttrib(tab,"contentid");
		var content=(cid)&&fdjtID(cid);
		if (!(content))
		    fdjtWarn("No reference for tab content %o",cid);
		else dropClass(content,shownclass);}
	    else dropClass(tab,shownclass);}
	if (fdjtID(contentid)) {
	    if (tabseen) addClass(contentid,shownclass);
	    else fdjtLog.warn("a tab for %s was not found in %o",
			      contentid,tabbar);}
	else fdjtLog.warn("No reference for tab content %o",contentid);}
    fdjtUI.Tabs.selectTab=select_tab;
    
    function setupTabs(elt){
	if (!(elt)) elt=fdjtDOM.$(".tabs[tabstate]");
	else if (typeof elt === 'string') elt=fdjtID(elt);
	if ((!(elt))||(!(elt.getAttribute("tabstate")))) return;
	var tabstate=elt.getAttribute("tabstate");
	var content_id=false;
	if (tabstate==='#') {
	    content_id=document.location.hash;
	    if (content_id[0]==='#') content_id=content_id.slice(1);
	    var content=((content_id)&&(fdjtID(content_id)));
	    if (!(content)) return;
	    var ss={}; fdjtUI.scrollSave(ss);
	    window.scrollTo(0,0);
	    if (!(fdjtDOM.isVisible(content)))
		fdjtUI.scrollRestore(ss);}
	else content_id=fdjtState.getQuery(tabstate)||
	    fdjtState.getCookie(tabstate);
	if (!(content_id)) return;
	if (content_id[0]==='#') content_id=content_id.slice(1);
	if (content_id) select_tab(elt,content_id);}
    fdjtUI.Tabs.setup=setupTabs;
    
    function selected_tab(tabbar){
	var tabs=fdjtDOM.getChildren(tabbar,".tab");
	var i=0; while (i<tabs.length) {
	    var tab=tabs[i++];
	    if (hasClass(tag,"shown"))
		return tag.getAttribute("contentid");}
	return false;}
    fdjtUI.Tabs.getSelected=selected_tab;}());


/* Delays */

(function(){
    var timeouts={};
    
    fdjtUI.Delay=function(interval,name,fcn){
	window.setTimeout(fcn,interval);};}());

/* Expansion */

fdjtUI.Expansion.toggle=function(evt,spec,exspec){
  evt=evt||event;
    var target=fdjtUI.T(evt);
    var wrapper=fdjtDOM.getParent(target,spec||".fdjtexpands");
    if (wrapper) fdjtDOM.toggleClass(wrapper,exspec||"fdjtexpanded");};
fdjtUI.Expansion.onclick=fdjtUI.Expansion.toggle;

fdjtUI.Collapsible.click=function(evt){
  evt=evt||event;
  var target=fdjtUI.T(evt);
  if (fdjtUI.isDefaultClickable(target)) return;
  var wrapper=fdjtDOM.getParent(target,".collapsible");
  if (wrapper) {
    fdjtUI.cancel(evt);
    fdjtDOM.toggleClass(wrapper,"expanded");};};

fdjtUI.Collapsible.focus=function(evt){
  evt=evt||event;
  var target=fdjtUI.T(evt);
  var wrapper=fdjtDOM.getParent(target,".collapsible");
  if (wrapper) {
    fdjtDOM.toggleClass(wrapper,"expanded");};};

/* Temporary Scrolling */

(function(){
    var saved_scroll=false;
    var use_native_scroll=false;
    var preview_elt=false;

    function scroll_discard(ss){
	if (ss) {
	    ss.scrollX=false; ss.scrollY=false;}
	else saved_scroll=false;}

    function scroll_save(ss){
	if (ss) {
	    ss.scrollX=window.scrollX; ss.scrollY=window.scrollY;}
	else {
	    if (!(saved_scroll)) saved_scroll={};
	    saved_scroll.scrollX=window.scrollX;
	    saved_scroll.scrollY=window.scrollY;}}
    
    function scroll_offset(wleft,eleft,eright,wright){
	var result;
	if ((eleft>wleft) && (eright<wright)) return wleft;
	else if ((eright-eleft)<(wright-wleft)) 
	    return eleft-Math.floor(((wright-wleft)-(eright-eleft))/2);
	else return eleft;}

    function scroll_into_view(elt,topedge){
	if ((topedge!==0) && (!topedge) && (fdjtDOM.isVisible(elt)))
	    return;
	else if ((use_native_scroll) && (elt.scrollIntoView)) {
	    elt.scrollIntoView(top);
	    if ((topedge!==0) && (!topedge) && (fdjtDOM.isVisible(elt,true)))
		return;}
	else {
	    var top = elt.offsetTop;
	    var left = elt.offsetLeft;
	    var width = elt.offsetWidth;
	    var height = elt.offsetHeight;
	    var winx=(window.pageXOffset||document.documentElement.scrollLeft||0);
	    var winy=(window.pageYOffset||document.documentElement.scrollTop||0);
	    var winxedge=winx+(document.documentElement.clientWidth);
	    var winyedge=winy+(document.documentElement.clientHeight);
	    
	    while(elt.offsetParent) {
		elt = elt.offsetParent;
		top += elt.offsetTop;
		left += elt.offsetLeft;}
	    
	    var targetx=scroll_offset(winx,left,left+width,winxedge);
	    var targety=
		(((topedge)||(topedge===0)) ?
		 ((typeof topedge === "number") ? (top+topedge) : (top)) :
		 (scroll_offset(winy,top,top+height,winyedge)));
	    
	    var vh=fdjtDOM.viewHeight();
	    var x=0; var y;
	    var y_target=top+(height/3);
	    if ((2*(height/3))<((vh/2)-50))
		y=y_target-vh/2;
	    else if ((height)<(vh-100))
		y=top-(50+(height/2));
	    else y=top-50;

	    window.scrollTo(x,y);}}

    fdjtUI.scrollTo=function(target,id,context,discard,topedge){
	scroll_discard(discard);
	if (id) document.location.hash=id;
	if (context) {
	    setTimeout(function() {
		scroll_into_view(context,topedge);
		if (!(fdjtDOM.isVisible(target))) {
		    scroll_into_view(target,topedge);}},
		       100);}
	else setTimeout(function() {scroll_into_view(target,topedge);},100);};

    function scroll_preview(target,context,delta){
	/* Stop the current preview */
	if (!(target)) {
	    stop_preview(); return;}
	/* Already previewing */
	if (target===preview_elt) return;
	if (!(saved_scroll)) scroll_save();
	if (typeof target === 'number')
	    window.scrollTo(((typeof context === 'number')&&(context))||0,target);
	else scroll_into_view(target,delta);
	preview_elt=target;}

    function scroll_restore(ss){
	if (preview_elt) {
	    preview_elt=false;}
	if ((ss) && (typeof ss.scrollX === "number")) {
	    // fdjtLog("Restoring scroll to %d,%d",ss.scrollX,ss.scrollY);    
	    window.scrollTo(ss.scrollX,ss.scrollY);
	    return true;}
	else if ((saved_scroll) &&
		 ((typeof saved_scroll.scrollY === "number") ||
		  (typeof saved_scroll.scrollX === "number"))) {
	    // fdjtLog("Restoring scroll to %o",_fdjt_saved_scroll);
	    window.scrollTo(saved_scroll.scrollX,saved_scroll.scrollY);
	    saved_scroll=false;
	    return true;}
	else return false;}

    function stop_preview(){
	fdjtDOM.dropClass(document.body,"preview");
	if ((preview_elt) && (preview_elt.className))
	    fdjtDOM.dropClass(preview_elt,"previewing");
	preview_elt=false;}

    fdjtUI.scrollSave=scroll_save;
    fdjtUI.scrollRestore=scroll_restore;
    fdjtUI.scrollIntoView=scroll_into_view;
    fdjtUI.scrollPreview=scroll_preview;
    fdjtUI.scrollRestore=scroll_restore;}());

(function(){
    function dosubmit(evt){
	evt=evt||event;
	var target=fdjtUI.T(evt);
	var form=fdjtDOM.getParent(target,"FORM");
	var submit_event = document.createEvent("HTMLEvents");
	submit_event.initEvent('submit',false,true);
	form.dispatchEvent(submit_event);
	form.submit();}
    fdjtUI.dosubmit=dosubmit;}());

(function(){
    var hasClass=fdjtDOM.hasClass;
    
    fdjtUI.T=function(evt) {
	evt=evt||event; return (evt.target)||(evt.srcElement);};

    fdjtUI.nodefault=function(evt){
	evt=evt||event;
	if (evt.preventDefault) evt.preventDefault();
	else evt.returnValue=false;
	return false;};

    fdjtUI.isClickable=function(target){
	if (target instanceof Event) target=fdjtUI.T(target);
	while (target) {
	    if (((target.tagName==='A')&&(target.href))||
		(target.tagName==="INPUT") ||
		(target.tagName==="TEXTAREA") ||
		(target.tagName==="SELECT") ||
		(target.tagName==="OPTION") ||
		(hasClass(target,"isclickable")))
		return true;
	    else if (target.onclick)
	      return true;
	    else target=target.parentNode;}
	return false;};

    fdjtUI.isDefaultClickable=function(target){
	if (target instanceof Event) target=fdjtUI.T(target);
	while (target) {
	    if (((target.tagName==='A')&&(target.href))||
		(target.tagName==="INPUT") ||
		(target.tagName==="TEXTAREA") ||
		(target.tagName==="SELECT") ||
		(target.tagName==="OPTION") ||
		(hasClass(target,"isclickable")))
		return true;
	    else target=target.parentNode;}
	return false;};

    fdjtUI.cancel=function(evt){
	evt=evt||event;
	if (evt.preventDefault) evt.preventDefault();
	else evt.returnValue=false;
	evt.cancelBubble=true;
	return false;};
    fdjtUI.nobubble=function(evt){
	evt=evt||event;
	evt.cancelBubble=true;};

}());

/* Emacs local variables
   ;;;  Local variables: ***
   ;;;  compile-command: "make; if test -f ../makefile; then cd ..; make; fi" ***
   ;;;  End: ***
*/
/* -*- Mode: Javascript; -*- */

var fdjt_ajax_id="$Id$";
var fdjt_ajax_version=parseInt("$Revision$".slice(10,-1));
fdjt_versions.decl("fdjt/ajax",fdjt_ajax_version);
fdjt_versions.decl("fdjt",fdjt_ajax_version);

/* Copyright (C) 2007-2011 beingmeta, inc.
   This file is a part of the FDJT web toolkit (www.fdjt.org)
   This file provides an abstraction layer for Ajax calls

   This program comes with absolutely NO WARRANTY, including implied
   warranties of merchantability or fitness for any particular
   purpose.

    Use, modification, and redistribution of this program is permitted
    under either the GNU General Public License (GPL) Version 2 (or
    any later version) or under the GNU Lesser General Public License
    (version 3 or later).

    These licenses may be found at www.gnu.org, particularly:
      http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
      http://www.gnu.org/licenses/lgpl-3.0-standalone.html
*/

var fdjtAjax=
    (function(){
	
	function compose_uri(base_uri,args){
	    var uri=base_uri; var need_amp=false;
	    if (base_uri[-1]==='&') need_amp=false;
	    else if (base_uri.indexOf('?')>=0) need_amp=true;
	    else uri=base_uri+"?";
	    var i=0; while (i<args.length) {
		if (!(args[i])) {i=i+2; continue;}
		uri=uri+((need_amp) ? ("&") : (""))+args[i]+"="+args[i+1];
		need_amp=true;
		i=i+2;}
	    return uri;}

	var trace_ajax=false;
	
	function fdjtAjax(callback,base_uri,args){
	    var req=new XMLHttpRequest();
	    req.onreadystatechange=function () {
		if ((req.readyState == 4) && (req.status == 200)) {
		    callback(req);}};
	    var uri=compose_uri(base_uri,args);
	    req.open("GET",uri,true);
	    req.withCredentials='yes';
	    req.send(null);
	    return req;}
	fdjtAjax.revid="$Id$";
	fdjtAjax.version=parseInt("$Revision$".slice(10,-1));

	fdjtAjax.textCall=function(callback,base_uri){
	    return fdjtAjax(function(req) {
		callback(req.responseText);},
			    base_uri,fdjtDOM.Array(arguments,2));};

	fdjtAjax.jsonCall=function(callback,base_uri){
	    return fdjtAjax(function(req) {
		callback(JSON.parse(req.responseText));},
			    base_uri,fdjtDOM.Array(arguments,2));};

	fdjtAjax.xmlCall=function(callback,base_uri){
	    return fdjtAjax(function(req) {
		callback(req.responseXML);},
			    base_uri,fdjtDOM.Array(arguments,2));};

	fdjtAjax.jsonpCall=function(uri,id,cleanup){
	    if ((id)&&($ID(id))) return false;
	    var script_elt=fdjtNewElement("SCRIPT");
	    if (id) script_elt.id=id;
	    if (cleanup) script_elt.oncleanup=cleanup;
	    script_elt.language='javascript';
	    script_elt.src=uri;
	    document.body.appendChild(script_elt);};

	fdjtAjax.jsonpFinish=function(id){
	    var script_elt=$ID(id);
	    if (!(script_elt)) return;
	    if (script_elt.oncleanup) script_elt.oncleanup();
	    fdjtDOM.remove(script_elt);};

	function add_query_param(parameters,name,value){
	    return ((parameters)?(parameters+"&"):(""))+
		name+"="+encodeURIComponent(value);}

	function formParams(form) {
	    fdjtUI.AutoPrompt.cleanup(form);
	    var parameters=false;
	    var inputs=fdjtDOM.getChildren(form,"INPUT");
	    var i=0; while (i<inputs.length) {
		var input=inputs[i++];
		if (!(input.disabled)) {
		    if (((input.type==="RADIO") || (input.type==="CHECKBOX")) ?
			(input.checked) : (true))
			parameters=add_query_param(parameters,input.name,input.value);
		    else parameters=add_query_param(parameters,input.name,input.value);}}
	    var textareas=fdjtDOM.getChildren(form,"TEXTAREA");
	    i=0; while (i<textareas.length) {
		var textarea=textareas[i++];
		if (!(textarea.disabled)) {
		    parameters=add_query_param(parameters,textarea.name,textarea.value);}}
	    var selectboxes=fdjtDOM.getChildren(form,"SELECT");
	    i=0; while (i<selectboxes.length) {
		var selectbox=selectboxes[i++]; var name=selectbox.name;
		var options=fdjtDOM.getChildren(selectbox,"OPTION");
		var j=0; while (j<options.length) {
		    var option=options[j++];
		    if (option.selected)
			parameters=add_query_param(parameters,name,option.value);}}
	    return parameters;}
	fdjtAjax.formParams=formParams;

	function add_field(result,name,value,multifields,downcase) {
	    if (downcase) name=name.toLowerCase();
	    if ((multifields)&&(fdjtKB.contains(multifields,name))) {
		if (result[name]) result[name].push(value);
		else result[name]=[value];}
	    else {
		var cur=result[name];
		if (!cur) result[name]=value;
		else if (cur instanceof Array) cur.push(value);
		else result[name]=[cur,value];}}

	function formJSON(form,multifields,downcase) {
	    fdjtUI.AutoPrompt.cleanup(form);
	    var result={};
	    var inputs=fdjtDOM.getChildren(form,"INPUT");
	    var i=0; while (i<inputs.length) {
		var input=inputs[i++];
		if ((!(input.disabled)) &&
		    (((input.type==="radio") || (input.type==="checkbox")) ?
		     (input.checked) : (true)))
		    add_field(result,input.name,input.value,multifields,downcase||false);}
	    var textareas=fdjtDOM.getChildren(form,"TEXTAREA");
	    i=0; while (i<textareas.length) {
		var textarea=textareas[i++];
		if (!(textarea.disabled)) 
		    add_field(result,textarea.name,textarea.value,multifields,downcase||false);}
	    var selectboxes=fdjtDOM.getChildren(form,"SELECT");
	    i=0; while (i<selectboxes.length) {
		var selectbox=selectboxes[i++]; var name=selectbox.name;
		var options=fdjtDOM.getChildren(selectbox,"OPTION");
		var j=0; while (j<options.length) {
		    var option=options[j++];
		    if (option.selected) add_field(result,name,option.value,multifields,downcase||false);}}
	    return result;}
	fdjtAjax.formJSON=formJSON;

	function ajaxSubmit(form,callback){
	    var ajax_uri=form.getAttribute("ajaxaction");
	    if (!(ajax_uri)) return false;
	    // Whether to do AJAX synchronously or not.
	    var syncp=form.getAttribute("synchronous");
	    if (!(callback)) {
		if (form.oncallback) callback=form.oncallback;
		else if (form.getAttribute("ONCALLBACK")) {
		    callback=new Function
		    ("req","form",input_elt.getAttribute("ONCALLBACK"));
		    form.oncallback=callback;}}
	    if (trace_ajax)
		fdjtLog("Direct %s AJAX submit to %s for %o with callback %o",
			((syncp)?("synchronous"):("asynchronous")),
			ajax_uri,form,callback);
	    // Firefox doesn't run the callback on synchronous calls
	    var success=false; var callback_run=false;
	    var req=new XMLHttpRequest();
	    var params=formParams(form);
	    fdjtDOM.addClass(form,"submitting");
	    if (form.method==="GET")
		req.open('GET',ajax_uri+"?"+params,(!(syncp)));
	    else req.open('POST',ajax_uri,(!(syncp)));
	    req.withCredentials='true';
	    req.onreadystatechange=function () {
		if (trace_ajax)
		    fdjtLog("Got callback (%d,%d) %o for %o, calling %o",
			    req.readyState,req.status,req,ajax_uri,callback);
		if ((req.readyState === 4) && (req.status>=200) && (req.status<300)) {
		    fdjtDOM.dropClass(form,"submitting");
		    success=true; 
		    if (callback) callback(req,form);
	      	    callback_run=true;}
		else {
		    if (trace_ajax)
			fdjtLog("Got callback (%d,%d) %o for %o, not calling %o",
				req.readyState,req.status,req,ajax_uri,callback);
		    callback_run=false;}};
	    try {
		if (form.method==="GET") req.send();
		else {
		    // Setting the content type will force some browsers into preflight,
		    //  which gets us stuck.
		    if (!(fdjtAjax.noctype))
			req.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
		    req.send(params);}
		success=true;}
	    catch (ex) {}
	    if ((syncp) && (!(callback_run))) {
		if (trace_ajax)
		    fdjtLog("Running callback (rs=%d,status=%d) %o for %o, calling %o",
			    req.readyState,req.status,req,ajax_uri,callback);
		if ((req.readyState === 4) && (req.status>=200) && (req.status<300)) {
		    fdjtDOM.dropClass(form,"submitting");
		    success=true;
		    if (callback) callback(req,form);}};
	    return success;}
	fdjtAjax.formSubmit=ajaxSubmit;

	function jsonpSubmit(form){
	    var jsonp_uri=form.getAttribute("jsonpuri");
	    if (!(jsonp_uri)) return false;
	    var success=false;
	    var jsonid=((form.id)?("JSONP"+form.id):("FORMJSONP"));
	    var params=formParams(form);
	    fdjtDOM.addClass(form,"submitting");
	    try {
		jsonpCall(jsonp_uri+"?"+params,jsonid,
			  function(){fdjtDropClass(form,"submitting")});}
	    catch (ex) {
		jsonpFinish(jsonid);
		fdjtLog.warn("Attempted JSONP call signalled %o",ex);
		return false;}
	    return true;}

	function form_submit(evt,callback){
	    evt=evt||event||null;
	    var form=fdjtUI.T(evt);
	    fdjtUI.AutoPrompt.cleanup(form);
	    if (fdjtDOM.hasClass(form,"submitting")) {
		fdjtDOM.dropClass(form,"submitting");
		return;}
	    // if (form.fdjtlaunchfailed) return;
	    form.fdjtsubmit=true;
	    fdjtDOM.addClass(form,"submitting");
	    if (ajaxSubmit(form,callback)) {
		// fdjtLog("Ajax commit worked");
		fdjtUI.cancel(evt);
		return false;}
	    else if (jsonpSubmit(form)) {
		// fdjtLog("Json commit worked");
		fdjtUI.cancel(evt);
		return false;}
	    else return;}

	function copy_args(args,i){
	    var lim=args.length; if (!(i)) i=0;
	    var copy=new Array(lim-i);
	    while (i<lim) {copy[i]=args[i]; i++;}
	    return copy;}

	/* Synchronous calls */
	function sync_get(callback,base_uri,args){
	    var req=new XMLHttpRequest();
	    var uri=compose_uri(base_uri,args);
	    req.open("GET",uri,false);
	    req.send(null);
	    if (callback) return callback(req);
	    else return req;}
	fdjtAjax.get=function(base_uri){
	    return sync_get(false,base_uri,copy_args(arguments,1));};
	fdjtAjax.getText=function(base_uri) {
	    return sync_get(function (req) { return req.responseText; },
			    base_uri,copy_args(arguments,1));};
	fdjtAjax.getJSON=function(base_uri) {
	    return sync_get(function (req) { return JSON.parse(req.responseText); },
			    base_uri,fdjtDOM.Array(arguments,1));};
	fdjtAjax.getXML=function(base_uri) {
	    return fdjtAjaxGet(function (req) { return JSON.parse(req.responseXML); },
			       base_uri,fdjtDOM.Array(arguments,1));};

	fdjtAjax.onsubmit=form_submit;
	fdjtAjax.noctype=true;

	return fdjtAjax;})();

/* Emacs local variables
;;;  Local variables: ***
;;;  compile-command: "make; if test -f ../makefile; then cd ..; make; fi" ***
;;;  End: ***
*/
/*
    http://www.JSON.org/json2.js
    2009-06-29

    Public Domain.

    NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.

    See http://www.JSON.org/js.html

    This file creates a global JSON object containing two methods: stringify
    and parse.

        JSON.stringify(value, replacer, space)
            value       any JavaScript value, usually an object or array.

            replacer    an optional parameter that determines how object
                        values are stringified for objects. It can be a
                        function or an array of strings.

            space       an optional parameter that specifies the indentation
                        of nested structures. If it is omitted, the text will
                        be packed without extra whitespace. If it is a number,
                        it will specify the number of spaces to indent at each
                        level. If it is a string (such as '\t' or '&nbsp;'),
                        it contains the characters used to indent at each level.

            This method produces a JSON text from a JavaScript value.

            When an object value is found, if the object contains a toJSON
            method, its toJSON method will be called and the result will be
            stringified. A toJSON method does not serialize: it returns the
            value represented by the name/value pair that should be serialized,
            or undefined if nothing should be serialized. The toJSON method
            will be passed the key associated with the value, and this will be
            bound to the object holding the key.

            For example, this would serialize Dates as ISO strings.

                Date.prototype.toJSON = function (key) {
                    function f(n) {
                        // Format integers to have at least two digits.
                        return n < 10 ? '0' + n : n;
                    }

                    return this.getUTCFullYear()   + '-' +
                         f(this.getUTCMonth() + 1) + '-' +
                         f(this.getUTCDate())      + 'T' +
                         f(this.getUTCHours())     + ':' +
                         f(this.getUTCMinutes())   + ':' +
                         f(this.getUTCSeconds())   + 'Z';
                };

            You can provide an optional replacer method. It will be passed the
            key and value of each member, with this bound to the containing
            object. The value that is returned from your method will be
            serialized. If your method returns undefined, then the member will
            be excluded from the serialization.

            If the replacer parameter is an array of strings, then it will be
            used to select the members to be serialized. It filters the results
            such that only members with keys listed in the replacer array are
            stringified.

            Values that do not have JSON representations, such as undefined or
            functions, will not be serialized. Such values in objects will be
            dropped; in arrays they will be replaced with null. You can use
            a replacer function to replace those with JSON values.
            JSON.stringify(undefined) returns undefined.

            The optional space parameter produces a stringification of the
            value that is filled with line breaks and indentation to make it
            easier to read.

            If the space parameter is a non-empty string, then that string will
            be used for indentation. If the space parameter is a number, then
            the indentation will be that many spaces.

            Example:

            text = JSON.stringify(['e', {pluribus: 'unum'}]);
            // text is '["e",{"pluribus":"unum"}]'


            text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t');
            // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]'

            text = JSON.stringify([new Date()], function (key, value) {
                return this[key] instanceof Date ?
                    'Date(' + this[key] + ')' : value;
            });
            // text is '["Date(---current time---)"]'


        JSON.parse(text, reviver)
            This method parses a JSON text to produce an object or array.
            It can throw a SyntaxError exception.

            The optional reviver parameter is a function that can filter and
            transform the results. It receives each of the keys and values,
            and its return value is used instead of the original value.
            If it returns what it received, then the structure is not modified.
            If it returns undefined then the member is deleted.

            Example:

            // Parse the text. Values that look like ISO date strings will
            // be converted to Date objects.

            myData = JSON.parse(text, function (key, value) {
                var a;
                if (typeof value === 'string') {
                    a =
/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);
                    if (a) {
                        return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4],
                            +a[5], +a[6]));
                    }
                }
                return value;
            });

            myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) {
                var d;
                if (typeof value === 'string' &&
                        value.slice(0, 5) === 'Date(' &&
                        value.slice(-1) === ')') {
                    d = new Date(value.slice(5, -1));
                    if (d) {
                        return d;
                    }
                }
                return value;
            });


    This is a reference implementation. You are free to copy, modify, or
    redistribute.

    This code should be minified before deployment.
    See http://javascript.crockford.com/jsmin.html

    USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO
    NOT CONTROL.
*/

/*jslint evil: true */

/*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply,
    call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours,
    getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join,
    lastIndex, length, parse, prototype, push, replace, slice, stringify,
    test, toJSON, toString, valueOf
*/

// Create a JSON object only if one does not already exist. We create the
// methods in a closure to avoid creating global variables.

var JSON = JSON || {};

(function () {

    function f(n) {
        // Format integers to have at least two digits.
        return n < 10 ? '0' + n : n;
    }

    if (typeof Date.prototype.toJSON !== 'function') {

        Date.prototype.toJSON = function (key) {

            return isFinite(this.valueOf()) ?
                   this.getUTCFullYear()   + '-' +
                 f(this.getUTCMonth() + 1) + '-' +
                 f(this.getUTCDate())      + 'T' +
                 f(this.getUTCHours())     + ':' +
                 f(this.getUTCMinutes())   + ':' +
                 f(this.getUTCSeconds())   + 'Z' : null;
        };

        String.prototype.toJSON =
        Number.prototype.toJSON =
        Boolean.prototype.toJSON = function (key) {
            return this.valueOf();
        };
    }

    var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
        escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
        gap,
        indent,
        meta = {    // table of character substitutions
            '\b': '\\b',
            '\t': '\\t',
            '\n': '\\n',
            '\f': '\\f',
            '\r': '\\r',
            '"' : '\\"',
            '\\': '\\\\'
        },
        rep;


    function quote(string) {

// If the string contains no control characters, no quote characters, and no
// backslash characters, then we can safely slap some quotes around it.
// Otherwise we must also replace the offending characters with safe escape
// sequences.

        escapable.lastIndex = 0;
        return escapable.test(string) ?
            '"' + string.replace(escapable, function (a) {
                var c = meta[a];
                return typeof c === 'string' ? c :
                    '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
            }) + '"' :
            '"' + string + '"';
    }


    function str(key, holder) {

// Produce a string from holder[key].

        var i,          // The loop counter.
            k,          // The member key.
            v,          // The member value.
            length,
            mind = gap,
            partial,
            value = holder[key];

// If the value has a toJSON method, call it to obtain a replacement value.

        if (value && typeof value === 'object' &&
                typeof value.toJSON === 'function') {
            value = value.toJSON(key);
        }

// If we were called with a replacer function, then call the replacer to
// obtain a replacement value.

        if (typeof rep === 'function') {
            value = rep.call(holder, key, value);
        }

// What happens next depends on the value's type.

        switch (typeof value) {
        case 'string':
            return quote(value);

        case 'number':

// JSON numbers must be finite. Encode non-finite numbers as null.

            return isFinite(value) ? String(value) : 'null';

        case 'boolean':
        case 'null':

// If the value is a boolean or null, convert it to a string. Note:
// typeof null does not produce 'null'. The case is included here in
// the remote chance that this gets fixed someday.

            return String(value);

// If the type is 'object', we might be dealing with an object or an array or
// null.

        case 'object':

// Due to a specification blunder in ECMAScript, typeof null is 'object',
// so watch out for that case.

            if (!value) {
                return 'null';
            }

// Make an array to hold the partial results of stringifying this object value.

            gap += indent;
            partial = [];

// Is the value an array?

            if (Object.prototype.toString.apply(value) === '[object Array]') {

// The value is an array. Stringify every element. Use null as a placeholder
// for non-JSON values.

                length = value.length;
                for (i = 0; i < length; i += 1) {
                    partial[i] = str(i, value) || 'null';
                }

// Join all of the elements together, separated with commas, and wrap them in
// brackets.

                v = partial.length === 0 ? '[]' :
                    gap ? '[\n' + gap +
                            partial.join(',\n' + gap) + '\n' +
                                mind + ']' :
                          '[' + partial.join(',') + ']';
                gap = mind;
                return v;
            }

// If the replacer is an array, use it to select the members to be stringified.

            if (rep && typeof rep === 'object') {
                length = rep.length;
                for (i = 0; i < length; i += 1) {
                    k = rep[i];
                    if (typeof k === 'string') {
                        v = str(k, value);
                        if (v) {
                            partial.push(quote(k) + (gap ? ': ' : ':') + v);
                        }
                    }
                }
            } else {

// Otherwise, iterate through all of the keys in the object.

                for (k in value) {
                    if (Object.hasOwnProperty.call(value, k)) {
                        v = str(k, value);
                        if (v) {
                            partial.push(quote(k) + (gap ? ': ' : ':') + v);
                        }
                    }
                }
            }

// Join all of the member texts together, separated with commas,
// and wrap them in braces.

            v = partial.length === 0 ? '{}' :
                gap ? '{\n' + gap + partial.join(',\n' + gap) + '\n' +
                        mind + '}' : '{' + partial.join(',') + '}';
            gap = mind;
            return v;
        }
    }

// If the JSON object does not yet have a stringify method, give it one.

    if (typeof JSON.stringify !== 'function') {
        JSON.stringify = function (value, replacer, space) {

// The stringify method takes a value and an optional replacer, and an optional
// space parameter, and returns a JSON text. The replacer can be a function
// that can replace values, or an array of strings that will select the keys.
// A default replacer method can be provided. Use of the space parameter can
// produce text that is more easily readable.

            var i;
            gap = '';
            indent = '';

// If the space parameter is a number, make an indent string containing that
// many spaces.

            if (typeof space === 'number') {
                for (i = 0; i < space; i += 1) {
                    indent += ' ';
                }

// If the space parameter is a string, it will be used as the indent string.

            } else if (typeof space === 'string') {
                indent = space;
            }

// If there is a replacer, it must be a function or an array.
// Otherwise, throw an error.

            rep = replacer;
            if (replacer && typeof replacer !== 'function' &&
                    (typeof replacer !== 'object' ||
                     typeof replacer.length !== 'number')) {
                throw new Error('JSON.stringify');
            }

// Make a fake root object containing our value under the key of ''.
// Return the result of stringifying the value.

            return str('', {'': value});
        };
    }


// If the JSON object does not yet have a parse method, give it one.

    if (typeof JSON.parse !== 'function') {
        JSON.parse = function (text, reviver) {

// The parse method takes a text and an optional reviver function, and returns
// a JavaScript value if the text is a valid JSON text.

            var j;

            function walk(holder, key) {

// The walk method is used to recursively walk the resulting structure so
// that modifications can be made.

                var k, v, value = holder[key];
                if (value && typeof value === 'object') {
                    for (k in value) {
                        if (Object.hasOwnProperty.call(value, k)) {
                            v = walk(value, k);
                            if (v !== undefined) {
                                value[k] = v;
                            } else {
                                delete value[k];
                            }
                        }
                    }
                }
                return reviver.call(holder, key, value);
            }


// Parsing happens in four stages. In the first stage, we replace certain
// Unicode characters with escape sequences. JavaScript handles many characters
// incorrectly, either silently deleting them, or treating them as line endings.

            cx.lastIndex = 0;
            if (cx.test(text)) {
                text = text.replace(cx, function (a) {
                    return '\\u' +
                        ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
                });
            }

// In the second stage, we run the text against regular expressions that look
// for non-JSON patterns. We are especially concerned with '()' and 'new'
// because they can cause invocation, and '=' because it can cause mutation.
// But just to be safe, we want to reject all unexpected forms.

// We split the second stage into 4 regexp operations in order to work around
// crippling inefficiencies in IE's and Safari's regexp engines. First we
// replace the JSON backslash pairs with '@' (a non-JSON character). Second, we
// replace all simple value tokens with ']' characters. Third, we delete all
// open brackets that follow a colon or comma or that begin the text. Finally,
// we look to see that the remaining characters are only whitespace or ']' or
// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval.

            if (/^[\],:{}\s]*$/.
test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@').
replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']').
replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {

// In the third stage we use the eval function to compile the text into a
// JavaScript structure. The '{' operator is subject to a syntactic ambiguity
// in JavaScript: it can begin a block or an object literal. We wrap the text
// in parens to eliminate the ambiguity.

                j = eval('(' + text + ')');

// In the optional fourth stage, we recursively walk the new structure, passing
// each name/value pair to a reviver function for possible transformation.

                return typeof reviver === 'function' ?
                    walk({'': j}, '') : j;
            }

// If the text is not JSON parseable, then a SyntaxError is thrown.

            throw new SyntaxError('JSON.parse');
        };
    }
}());

