/* -*- Mode: Javascript; Character-encoding: utf-8; -*- */

/* Copyright (C) 2009 beingmeta, inc.
   This file was created from several component files, some of
      which have different restrictions.

   For purposes of inclusion of this code in non-commercial web documents,
     use and redistribution of this file is permitted under the terms of
     the Creative Commons "Attribution-NonCommercial" license:
          http://creativecommons.org/licenses/by-nc/3.0/ 

   For all other purposes, the contents of this file are licensed
    under the terms of the nearest preceding copyright notice.  The
    copyright notices of the individual files are all prefixed by
    a line of the form "Copyright (C) ...".

    Other uses may be allowed based on prior agreement with
      beingmeta, inc.  Inquiries can be addressed to:

       licensing@biz.beingmeta.com

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

*/

var fdjt_revision='1.5-98-gbf0cc8a';
var fdjt_buildhost='dev.beingmeta.com';
var fdjt_buildtime='Sun Dec 11 15:19:46 UTC 2011';
var sbooks_buildhost='dev.beingmeta.com';
var sbooks_buildtime='Mon Dec 12 17:30:44 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;};

// Some augmentations
if (!(Array.prototype.indexOf))
    Array.prototype.indexOf=function(elt,i){
	if (!(i)) i=0; var len=this.length;
	while (i<len) if (this[i]===elt) return i; else i++;
	return -1;};
// https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/keys
if(!Object.keys) {
    Object.keys = function(o){
	if (o !== Object(o))
            throw new TypeError('Object.keys called on non-object');
	var ret=[],p;
	for(p in o) if(Object.prototype.hasOwnProperty.call(o,p)) ret.push(p);
	return ret;}};

if (!String.prototype.trim) {
    String.prototype.trim = (function () {
	var trimLeft  = /^\s+/,
        trimRight = /\s+$/

	return function () {
	    return this.replace(trimLeft, "").replace(trimRight, "")
	}
    })()};

/* -*- Mode: Javascript; -*- */

/* 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)||(string.search("%j")))&&
	    (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 timespan=fdjtDOM("span.time",fdjtET());
	    var entry=fdjtDOM("div.fdjtlog");
	    if (output) entry.innerHTML=output;
	    else entry.innerHTML=fdjtString.apply(null,arguments);
	    fdjtDOM.prepend(entry,timespan);
	    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;

/**
 * HumaneJS
 * Humanized Messages for Notifications
 * @author Marc Harter (@wavded)
 * @contributers
 *   Alexander (@bga_)
 *   Jose (@joseanpg)
 * @example
 *  humane('hello world');
 */
;(function(win,doc){
    var eventOn, eventOff;
    if (win.addEventListener) {
	eventOn = function(obj,type,fn){obj.addEventListener(type,fn,false)};
	eventOff = function(obj,type,fn){obj.removeEventListener(type,fn,false)};
    } else {
	eventOn = function(obj,type,fn){obj.attachEvent('on'+type,fn)};
	eventOff = function(obj,type,fn){obj.detachEvent('on'+type,fn)};
    }

    var eventing = false;
    var animationInProgress = false;
    var humaneEl = null;
    // Table mapping msg node IDs into the nodes themselves
    var msgnodes={};
    var timeout = null;
    // ua sniff for filter support
    var useFilter = /msie [678]/i.test(navigator.userAgent);
    var isSetup = false;
    var queue = [];

    eventOn(win,'load',function(){
        var transitionSupported = (function(style){
            var prefixes = ['MozT','WebkitT','OT','msT','KhtmlT','t'];
            for(var i = 0, prefix; prefix = prefixes[i]; i++){
                if(prefix+'ransition' in style) return true;
            }
            return false;
        }(doc.body.style));

        if(!transitionSupported) animate = jsAnimateOpacity; // override animate
        setup();
        run();
    });

    function setup() {
	var probe=doc.getElementById('HUMANE');
	if (probe) humaneEl=probe;
	else {
            humaneEl = doc.createElement('div');
            humaneEl.id = 'HUMANE';
            humaneEl.className = 'humane';
            doc.body.appendChild(humaneEl);}
        if(useFilter) humaneEl.filters.item('DXImageTransform.Microsoft.Alpha').Opacity = 0; // reset value so hover states work
        isSetup = true;
    }

    function remove() {
        eventOff(doc.body,'mousemove',remove);
        eventOff(doc.body,'click',remove);
        eventOff(doc.body,'keypress',remove);
        eventOff(doc.body,'touchstart',remove);
        eventing = false;
        if(animationInProgress) animate(0);
    }

    function run() {
        if(animationInProgress && !fdjtLog.notify.forceNew) return;
        if(!queue.length){
            remove();
            return;
        }

        animationInProgress = true;

        if(timeout){
            clearTimeout(timeout);
            timeout = null;
        }

        timeout = setTimeout(function(){
	    // allow notification to stay alive for timeout
            if(!eventing){
                eventOn(doc.body,'mousemove',remove);
                eventOn(doc.body,'click',remove);
                eventOn(doc.body,'keypress',remove);
                eventOn(doc.body,'touchstart',remove);
                eventing = true;
                if(!fdjtLog.notify.waitForMove) remove();
            }
        }, fdjtLog.notify.timeout);

	var msg=queue.shift();
	if (msg.nodeType) {
	    humaneEl.innerHTML = "";
	    humaneEl.appendChild(msg);}
	else if (typeof msg !== 'string')
	    throw new Exception("Bad arg to Humane");
	else if ((msg.length>1)&&(msg[0]==='#')) {
	    var nodeid=msg.slice(1);
	    node=msgnodes[nodeid];
	    if ((!(node))&&(node=document.getElementById(nodeid)))
		msgnodes[nodeid]=node;
	    if (node) {
		humaneEl.innerHTML = "";
		humaneEl.appendChild(node);}
	    else humaneEl.innerHTML = msg;}
	else humaneEl.innerHTML = msg;
        animate(1);
    }

    function animate(level){
        if(level === 1){
            humaneEl.className = "humane humane-show";
        } else {
            humaneEl.className = "humane";
            end();
        }
    }

    function end(){
        animationInProgress = false;
        setTimeout(run,500);
    }

    // if CSS Transitions not supported, fallback to JS Animation
    var setOpacity = (function(){
        if(useFilter){
            return function(opacity){
                humaneEl.filters.item('DXImageTransform.Microsoft.Alpha').Opacity = opacity*100;
            }
        } else {
            return function(opacity){
                humaneEl.style.opacity = String(opacity);
            }
        }
    }());
    function jsAnimateOpacity(level,callback){
        var interval;
        var opacity;

        if (level === 1) {
            opacity = 0;
            if(fdjtLog.notify.forceNew){
                opacity = useFilter ? humaneEl.filters.item('DXImageTransform.Microsoft.Alpha').Opacity/100|0 : humaneEl.style.opacity|0;
            }
            humaneEl.style.visibility = "visible";
            interval = setInterval(function(){
                if(opacity < 1) {
                    opacity +=0.1;
                    if (opacity>1) opacity = 1;
                    setOpacity(opacity);
                }
                else {
                    clearInterval(interval);
                }
            }, 500 / 20);
        } else {
            opacity = 1;
            interval = setInterval(function(){
                if(opacity > 0) {
                    opacity -=0.1;
                    if (opacity<0) opacity = 0;
                    setOpacity(opacity);
                }
                else {
                    clearInterval(interval);
                    humaneEl.style.visibility = "hidden";
                    end();
                }
            }, 500 / 20);
        }
    }

    function notify(message){
	fdjtLog.apply(null,arguments);
	if (arguments.length>1)
	    message=fdjtString.apply(null,arguments);
        queue.push(message);
        if(isSetup) run();}

    function msg(message){
	if (!(message)) {
            if(!eventing){
                eventOn(doc.body,'mousemove',remove);
                eventOn(doc.body,'click',remove);
                eventOn(doc.body,'keypress',remove);
                eventOn(doc.body,'touchstart',remove);
                eventing = true;}
	    animationInProgress=true;
	    animate(1);
	    return;}
	if (arguments.length>1)
	    message=fdjtString.apply(null,arguments);
        queue.push(message);
        if(isSetup) run();}

    fdjtLog.notify = notify;
    fdjtLog.notify.timeout = 2000;
    fdjtLog.notify.waitForMove = true;
    fdjtLog.notify.forceNew = false;

    fdjtLog.Humane=msg;
    fdjtLog.HumaneHide=remove;

}(window,document));


var fdjtNotify=fdjtLog.notify;

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

/* 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){
	    if ((typeof string !== 'string')&&
		(!(string instanceof String)))
		return stringify(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 ((string[cmd+1]==='x')&&
			 (typeof arguments[i] === 'number')&&
			 (arguments[i]>=0)&&
			 ((arguments[i]%1)>=0)) {
		    var arg=arguments[i++];
		    output=output+arg.toString(16);}
		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)||arg.srcElement)) {
		var target=arg.target||arg.srcElement;
		var ox=arg.offsetX, oy=arg.offsetY;
		var result="["+arg.type+"@"+stringify(target)+"(m="+
		    (((arg.shiftKey===true)?"s":"")+
		     ((arg.ctrlKey===true)?"c":"")+
		     ((arg.altKey===true)?"a":"")+
		     (arg.button||0));
		if (ox) result=result+",x="+ox+",y="+oy;
		else if (arg.touches) {
		    var i=0; var n=arg.touches.length;
		    result=result+",touches="+n;}
		else if ((arg.keyCode)||(arg.charCode))
		    result=result+",kc="+arg.keyCode+",cc="+arg.charCode;
		return result+")]";}
	    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 normstring(string){
	    return string.replace(/\W*\s\W*/g," ").toLowerCase();}
	fdjtString.normString=normstring;

	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;

	function paraHash(node){
	    var text=node.innerText;
	    var words=text.split(/\W*\S+\W*/g);
	    var len=words.length;
	    return "_H"+
		((len>0)?(words[0][0]):".")+
		((len>1)?(words[1][0]):".")+
		((len>2)?(words[2][0]):".")+
		((len>3)?(words[3][0]):".")+
		((len>0)?(words[len-1][0]):".")+
		((len>1)?(words[len-2][0]):".")+
		((len>2)?(words[len-3][0]):".")+
		((len>3)?(words[len-4][0]):".");}
	fdjtString.paraHash=paraHash;

	return fdjtString;})();

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

/* 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){
	    if (typeof tstamp === 'number') {
		if (tstamp<131592918600)
		    tstamp=new Date(tstamp*1000);
		else tstamp=new Date(tstamp);}
	    return tstamp.toDateString();};
	fdjtTime.timeString=function(tstamp){
	    if (typeof tstamp === 'number') {
		if (tstamp<131592918600)
		    tstamp=new Date(tstamp*1000);
		else tstamp=new Date(tstamp);}
	    return tstamp.toString();};

	function shortString(tstamp){
	    var now=new Date();
	    if (typeof tstamp === 'number') {
		if (tstamp<131592918600)
		    tstamp=new Date(tstamp*1000);
		else tstamp=new Date(tstamp);}
	    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===0)?"00":(((minutes<10)?"0":"")+minutes));}}
	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,watch,done,slice,space){
	    var i=0; var lim=vec.length; var chunks=0;
	    var used=0; var zerostart=fdjtTime();
	    if (!(slice)) slice=100;
	    if (!(space)) space=slice;
	    var stepfn=function(){
		var started=fdjtTime(); var now=started;
		var stopat=started+slice;
		if (watch) watch(((i==0)?'start':'resume'),i,lim,chunks,used,zerostart);
		while ((i<lim)&&((now=fdjtTime())<stopat)) {
		    var elt=vec[i];
		    if (watch) watch('element',i,lim,elt,used,now-zerostart);
		    fn(elt);
		    if (watch)
			watch('after',i,lim,elt,used+(fdjtTime()-started),
			      zerostart,fdjtTime()-now);
		    i++;}
		chunks=chunks+1;
		if (i<lim) {
		    used=used+(now-started);
		    if (watch) watch('suspend',i,lim,chunks,used,zerostart);
		    setTimeout(stepfn,space);}
		else {
		    now=fdjtTime(); used=used+(now-started);
		    if (done) {
			if (watch) watch('finishing',i,lim,chunks,used,zerostart);
			done();}
		    var donetime=((done)&&(fdjtTime()-now));
		    now=fdjtTime(); used=used+(now-started);
		    if (watch) watch('done',i,lim,chunks,used,zerostart,donetime);}};
	    setTimeout(stepfn,space);}
	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; -*- */

/* 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 fdjt$=false;

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;
		if ((NodeList)&&(content instanceof NodeList)) content=TOA(content);
		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;
		if ((NodeList)&&(content instanceof NodeList)) content=TOA(content);
		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);}

	fdjtDOM.appendArray=domappend;
	
	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 (typeof node === "string") node=fdjtID(node);
	    if (!(node)) return [];
	    if (!(results)) results=[]; 
	    if (!(attrib)) {
		if (typeof classname === 'function')
		    filter_children(node,classname,results);
		else 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 if (!(typeof attrib === 'string'))
		throw { error: 'bad selector arg', selector: classname};
	    else {
		var pat=(classpats[classname]||classPat(classname));
		gather_children(node,classname,attrib||false,results);}
	    return results;}
	fdjtDOM.getChildren=getChildren;
	fdjt$=fdjtDOM.$=function(spec,root){
	    return toArray(getChildren(root||document,spec));};
	fdjt$1=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 (after) {
	    if (typeof after === 'string')
		after=document.getElementById(after);
	    if (after.nextSibling)
		dominsert(after.nextSibling,arguments,1);
	    else domappend(after.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++;}}
	    if ((!type)||(type==='select')) {
		var inputs=root.getElementsByTagName('select');
		var i=0; var lim=inputs.length;
		while (i<lim) {
		    if ((!(name))||(inputs[i].name===name))
			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;
	    if (elt.nodeType!==1) throw "Not an element";
	    try {
		var style=
		    ((window.getComputedStyle)&&
		     (window.getComputedStyle(elt,null)))||
		    (elt.currentStyle);
		if (!(style)) return false;
		else if (prop) return style[prop];
		else return style;}
	    catch (ex) {
		fdjtLog("Unexpected style error %o",ex);
		return false;}}
	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=p+": "+v;
		else result=result+"; "+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){
	    if ((!(elt))||(!(elt.nodeType))||(elt.nodeType!==1))
		return false;
	    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,outer,withstack){
	    if (!(withstack)) withstack=false;
	    if (typeof elt === 'string')
		elt=document.getElementById(elt);
	    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) 
		return {left: 0,top: 0,width:width,height: height};
	    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;}
	    
	    if (outer) {
		var outer_width, outer_height;
		var style=getStyle(elt);
		var t_margin=parsePX(style.marginTop);
		var r_margin=parsePX(style.marginRight);
		var b_margin=parsePX(style.marginBottom);
		var l_margin=parsePX(style.marginLeft);
		outer_width=width+l_margin+r_margin;
		outer_height=height+t_margin+b_margin;
		return {left: left, top: top, width: width,height: height,
			right:left+width,bottom:top+height,
			top_margin: t_margin, bottom_margin: b_margin,
			left_margin: l_margin, right_margin: r_margin,
			outer_height: outer_height,outer_width: outer_width,
			stack:withstack};}
	    else return {left: left, top: top, width: width,height: height,
			 right:left+width,bottom:top+height,
			 stack:withstack};}
	fdjtDOM.getGeometry=getGeometry;

	function geomString(geom){
	    return +((typeof geom.width == 'number')?(geom.width):"?")+
		"x"+((typeof geom.height == 'number')?(geom.height):"?")+
		"@l:"+((typeof geom.left == 'number')?(geom.left):"?")+
		",t:"+((typeof geom.top == 'number')?(geom.top):"?")+
		"/r:"+((typeof geom.right == 'number')?(geom.right):"?")+
		",b:"+((typeof geom.bottom == 'number')?(geom.bottom):"?");}
	fdjtDOM.geomString=geomString;

	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 countBreaks(arg){
	    if (typeof arg === 'string') {
		return arg.match(/\W*\s+\W*/g).length;}
	    else if (!(arg.nodeType)) return 0;
	    else if (arg.nodeType===1) {}
	    else if (arg.nodeType===3)
		return arg.nodeValue.match(/\W*\s+\W*/g).length;
	    else return 0;}
	fdjtDOM.countBreaks=countBreaks;

	function wordOffset(arg){
	    var scan=arg; var count=0;
	    while (scan=(scan.previousSibling||scan.parentNode)) {
		if (scan.nodeType===3)
		    count=count+(scan.nodeValue.match(/\W*\s+\W*/g).length);
		else if (scan.nodeType===1)
		    count=count+countBreaks(scan);
		else {}}
	    return count;}

	function hasContent(node,recur,test){
	    if (node===recur) return false;
	    else if (node.nodeType===3)
		return (child.nodeValue.search(/\w/g)>=0);
	    else if (node.nodeType!==1) return false;
	    else if ((test)&&(test.match)&&(test.match(node)))
		return true;
	    else if ((test===true)&&
		     ((node.tagName==='IMG')||
		      (node.tagName==='OBJECT')))
		return true;
	    else if (node.childNodes) {
		var children=node.childNodes;
		var i=0; while (i<children.length) {
		    var child=children[i++];
		    if (child===recur) return false;
		    else if (child.nodeType===3) {
			if (child.nodeValue.search(/\w/g)>=0) return true;
			else continue;}
		    else if (child.nodeType!==1) continue;
		    else if (recur) {
			if (hasContent(child,recur,test)) return true;
			else continue;}
		    else continue;}
		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 adjustInside(elt,container,step,min,pad){
	    var trace_adjust=(elt.traceadjust)||
		(container.traceadjust)||fdjtDOM.trace_adjust||
		((elt.className)&&(elt.className.search(/\btraceadjust\b/)>=0))||
		((container.className)&&
		 (container.className.search(/\btraceadjust\b/)>=0))||
		default_trace_adjust;
	    if (!(step)) step=5;
	    if (!(min)) min=50;
	    if (!(pad)) pad=1;
	    var scale=100;
	    function adjust(){
		var outside=getGeometry(container);
		var inside=getGeometry(elt,container);
		var style=getStyle(container);
		var maxwidth=
		    outside.width-
		    (parsePX(style.paddingLeft,0)+
		     parsePX(style.borderLeft,0)+
		     parsePX(style.paddingRight,0)+
		     parsePX(style.borderRight,0));
		var maxheight=
		    outside.height-
		    (parsePX(style.paddingTop,0)+
		     parsePX(style.borderTop,0)+
		     parsePX(style.paddingBottom,0)+
		     parsePX(style.borderBottom,0));
		if (trace_adjust)
		    fdjtLog("adjustInside scale=%o step=%o min=%o pad=%o [l%o,t%o,r%o,b%o] << %ox%o < %ox%o",
			    scale,step,min,pad,
			    inside.left,inside.top,inside.right,inside.bottom,
			    maxwidth*pad,maxheight*pad,
			    maxwidth,maxheight);
		if ((inside.top>=0)&&(inside.bottom<=(pad*maxheight))&&
		    (inside.left>=0)&&(inside.right<=(pad*maxwidth)))
		    return;
		else if (scale<=min) return;
		else {
		    scale=scale-step;
		    applyScale(elt,scale,trace_adjust);
		    setTimeout(adjust,10);}}
	    setTimeout(adjust,10);}
	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)+
		padding;
	    var vpadding=
		(fdjtDOM.parsePX(style.paddingTop)||0)+
		(fdjtDOM.parsePX(style.paddingBottom)||0)+
		(fdjtDOM.parsePX(style.borderTopWidth)||0)+
		(fdjtDOM.parsePX(style.borderBottomWidth)||0)+
		padding;
	    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.adjustInside=adjustInside;
	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)));};

	/* Stylesheet manipulation */

	// Adapted from 
	// http://www.hunlock.com/blogs/Totally_Pwn_CSS_with_Javascript

        // Return requested style object
	function getCSSRule(ruleName, deleteFlag) {
	    ruleName=ruleName.toLowerCase();
            // If browser can play with stylesheets
	    if (document.styleSheets) {
		// For each stylesheet
		for (var i=0; i<document.styleSheets.length; i++) {
		    var styleSheet=document.styleSheets[i];
		    var cssRule=false;
		    var cssRules=styleSheet.cssRules||styleSheet.rules;
		    var n_rules=((cssRules)&&(cssRules.length));
		    var ii=0; while (ii<n_rules) {
			if (cssRules[ii])  {
			    var cssRule=cssRules[ii];
			    if (cssRule.selectorText.toLowerCase()==ruleName) {
				if (deleteFlag=='delete') {
				    if (styleSheet.cssRules) {
					styleSheet.deleteRule(ii);}
				    // Delete rule IE style.
				    return true;}
				// found and not deleting.
				else {return cssRule;}
                                // end found cssRule
			    }}   
			ii++;}
		    /* end for stylesheets */ }
		return false;}
	    return false;}
	fdjtDOM.getCSSRule=getCSSRule;

	function dropCSSRule(ruleName) {// Delete a CSS rule   
	    return getCSSRule(ruleName,'delete');}
	fdjtDOM.dropCSSRule=dropCSSRule;

	function addCSSRule(selector,text) {// Create a new css rule
	    var styles=fdjtID("FDJTSTYLES");
	    if (!(styles)) {
		var head=document.getElementsByTagName("HEAD");
		if (head.length===0) return; else head=head[0];
		styles=fdjtDOM("style#FDJTSTYLES");
		head.appendChild(styles);}
	    var sheet=styles.sheet;
	    if (sheet.insertRule) {
		var rules=sheet.cssRules||sheet.rules;
		var i=0; var lim=rules.length;
		while (i<lim) {
		    var rule=rules[i];
		    if (rule.selectorText===selector) break;
		    else i++;}
		if (i<lim) {
		    if (sheet.deleteRule) sheet.deleteRule(i);
		    else if (sheet.removeRule) sheet.removeRule(i);
		    else {}}
		var rules=sheet.cssRules||sheet.rules;
		var ruletext=selector+' {'+text+'}';
		if (sheet.insertRule)
		    sheet.insertRule(ruletext, rules.length);
		else if (sheet.addRule)
		    sheet.addRule(selector,text);
		else return false;
		return ruletext;}
	    else return false;}
	fdjtDOM.addCSSRule=addCSSRule;

	/* 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 (var 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';}
	
	function node2text(node,accum){
	    if (!(accum)) accum="";
	    if (node.nodeType===3) {
		var stringval=node.nodeValue;
		if (stringval) accum=accum+stringval;
		return accum;}
	    else if (node.nodeType===1) {
		var children=node.childNodes;
		var i=0, lim=children.length;
		while (i<lim) {
		    accum=node2text(children[i++],accum);}
		return accum;}
	    else return accum;}
	fdjtDOM.node2text=node2text;
	
	function get_text_pos(node,pos,cur){
	    if (cur>pos) return false;
	    else if (node.nodeType===3) {
		var stringval=node.nodeValue;
		if (pos<(cur+stringval.length))
		    return { node: node, off: pos-cur};
		else return pos+stringval.length;}
	    else if (node.nodeType===1) {
		var children=node.childNodes;
		var i=0, lim=children.length;
		while (i<lim) {
		    cur=get_text_pos(children[i++],pos,cur);
		    if (!(typeof cur === 'number')) return cur;}
		return cur;}
	    else return cur;}
	fdjtDOM.textPos=function(node,pos){
	    return get_text_pos(node,pos,0);};

	function findExcerpt(node,string,count){
	    if (!(count)) count=1;
	    var fulltext=node2text(node); var loc=-1, cur=0;
	    while ((loc=fulltext.indexOf(string,cur))>=0) {
		if (count===1) {
		    var start=get_text_pos(node,loc,0);
		    var end=get_text_pos(node,loc+string.length,0);
		    return { start_node: start.node, start_off: start.off,
			     end_node: end.node, end_off: end.off};}
		else {count--; cur=loc+string.length;}}
	    return false;}
	fdjtDOM.findExcerpt=findExcerpt;

	var docuri=false; var docbase=false;
	function init_docuri(){
	    if (docuri) return;
	    docuri=getLink("refuri")||getLink("canonical")||
		document.location.href;
	    docbase=getMeta("baseid");}
	

	function getAssignedIDs(node){
	    var refuris=[];
	    var baseids=[];
	    var scan=node;
	    var refuri=false;
	    while (scan) {
		if ((scan)&&(scan.getAttribute)&&
		    ((refuri=scan.getAttribute("data-refuri"))||
		     (refuri=scan.getAttribute("refuri")))) {
		    var baseid=scan.getAttribute("data-baseid")||
			scan.getAttribute("baseid")||
			false;
		    refuris.push(refuri); baseids.push(baseid);
		    scan=scan.parentNode;}
		else scan=scan.parentNode;}
	    var link=getLink("refuri")||getLink("canonical");
	    var base=getLink("baseid");
	    if (docuri) init_docuri();
	    refuris.push(docuri);
	    baseids.push(docbase);
	    var ids=[];
	    if (node.id) ids.push(node.id);
	    if (node.childNodes) {
		var children=node.childNodes, child;
		var i=0; var lim=children.length;
		while (i<lim) {
		    if (((child=children[i++]).nodeType===1)&&
			(child.tagName==='A')) {
			if (child.name) ids.push(child.name);
			else if (child.id) ids.push(child.id);}}}
	    var refs=[];
	    var i=0; var lim=ids.length;
	    while (i<lim) {
		var id=ids[i++];
		var j=0; var jlim=refuris.length;
		while (j<jlim) {
		    if ((!(baseids[j]))||
			(id.search(baseids[j])===0))
			refs.push(refuris[j]+"#"+id);
		    j++;}}
	    return refs;}

	fdjtDOM.getParaHash=function(node){
	    return paraHash(textify(node));}

	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; -*- */

/* 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._id||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._id)||(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={};
	    // Whether _id fields in this pool are 'absolute' (globally unique)
	    this.absref=false; 
	    return this;}
	fdjtKB.Pool=Pool;
	fdjtKB.PoolRef=function(name,create){
	    if (!(name)) return this;
	    if (pools[name]) return pools[name];
	    else if (!(create)) return false;
	    else return new Pool(name);};
	
	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._id)) cons._id=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;
		while (i<lim) this.Import(data[i++]);
		return;}
	    else {
		var qid=data._id||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,pool){
	    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 (pool)
		return pool.ref(arg);
	    else if (arg.search(uuid_pattern)===0) {
		var uuid_type=arg.slice(34);
		var pool=fdjtKB.PoolRef("-UUIDTYPE="+uuid_type);
		if (pool) return pool.ref(arg);
		else return false;}
	    else if ((arg[0]===':')&&(arg[1]==='#')&&(arg[2]==='U')&&
		     (arg.search(uuid_pattern)===3)) {
		var uuid_type=arg.slice(37); var uuid=arg.slice(3);
		if ((pool)&&(pool.ref(uuid))) return pool.ref(uuid);
		var pool=fdjtKB.PoolRef("-UUIDTYPE="+uuid_type);
		if (pool) return pool.ref(uuid);
		return false;}
	    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 (pool) return pool.ref(arg);
	    else return false;}
	
	function getRef(arg,pool){
	    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,pool);
	    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._id||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 arr_contains(arr,val,start){
	    return (arr.indexOf(val,start||0)>=0);}
	function arr_position(arr,val,start){
	    return arr.indexOf(val,start||0);}

	/* 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._id)
		    if (b._id)
			if (a._id<b._id) return -1;
		else if (a._id===b._id) return 0;
		else return 1;
		else return 1;
		else if (b._id) 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 (typeof set1 === 'string') set1=[set1];
	    if (typeof set2 === 'string') set2=[set2];
	    if ((!(set1))||(set1.length===0)) return [];
	    if ((!(set2))||(set2.length===0)) return [];
	    if (set1._sortlen!==set1.length) set1=Set(set1);
	    if (set2._sortlen!==set2.length) set2=Set(set2);
	    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 difference(set1,set2){
	    if (typeof set1 === 'string') set1=[set1];
	    if (typeof set2 === 'string') set2=[set2];
	    if ((!(set1))||(set1.length===0)) return [];
	    if ((!(set2))||(set2.length===0)) return set1;
	    if (set1._sortlen!==set1.length) set1=Set(set1);
	    if (set2._sortlen!==set2.length) set2=Set(set2);
	    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]) {
		    i++; j++;}
		else if ((allstrings)?
			 (set1[i]<set2[j]):
			 (set_sortfn(set1[i],set2[j])<0)) {
		    if ((new_allstrings)&&(typeof set1[i] !== 'string'))
			new_allstrings=false;
		    results.push(set1[i]);
		    i++;}
		else j++;}
	    results._allstrings=new_allstrings;
	    results._sortlen=results.length;
	    return results;}
	fdjtKB.difference=difference;
	
	function union(set1,set2){
	    if (typeof set1 === 'string') set1=[set1];
	    if (typeof set2 === 'string') set2=[set2];
	    if ((!(set1))||(set1.length===0)) return set2;
	    if ((!(set2))||(set2.length===0)) return set1;
	    if (set1._sortlen!==set1.length) set1=Set(set1);
	    if (set2._sortlen!==set2.length) set2=Set(set2);
	    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 (typeof set1 === 'string') set1=[set1];
	    if (typeof set2 === 'string') set2=[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 (typeof set1 === 'string') set1=[set1];
	    if (typeof set2 === 'string') set2=[set2];
	    if ((!(set1))||(set1.length===0)) return false;
	    if ((!(set2))||(set2.length===0)) return false;
	    if (set1._sortlen!==set1.length) set1=Set(set1);
	    if (set2._sortlen!==set2.length) set2=Set(set2);
	    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;}}
	
	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._id||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._id||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._id||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 (arr_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 (arr_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._id||key.oid||key.uuid||key._fdjtid||register(key)];
		else delete this.scalar_map[key];}
	    else if (isobject(key)) {
		var objkey=key._id||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=arr_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=arr_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={};
	    var dontindex=false;
	    var index=function(item,prop,val,add){
		var valkey; var indices=scalar_indices;
		if (!(prop))
		    return {
			scalars: scalar_indices,
			objects: object_indices};
		else if ((dontindex)?(dontindex[prop]):(prop[0]==='_'))
		    return false;
		else if (!(val))
		    return {
			scalars: scalar_indices[prop],
			objects: object_indices[prop]};
		else if (isobject(val)) {
		    valkey=val._id||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._id||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=arr_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;};
	    return index;}
	fdjtKB.Index=Index;

	/* Refs */

	function Ref(pool,qid) {
	    if (pool) this.pool=pool;
	    if (qid) this._id=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 (arr_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)) {
		this.update_ref(data);
		return;}
	    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
	function update_ref(data){
	    var pool=this.pool; var map=pool.map;
	    for (var key in data) {
		if (key==="pool") continue;
		var val=data[key], cur=this[key];
		if (val===cur) continue;
		else if (!(cur)) {
		    if (val instanceof Array) {
			var i=0, lim=val.length;
			while (i<lim) this.add(key,val[i++]);}
		    else this.add(key,val);}
		else if ((val instanceof Array)||
			 (cur instanceof Array)) {
		    var toadd=difference(val,cur);
		    var todrop=difference(cur,val);
		    var i=0; var lim=todrop.length;
		    while (i<lim) this.drop(key,todrop[i++]);
		    var i=0; var lim=toadd.length;
		    while (i<lim) this.add(key,toadd[i++]);}
		else {
		    this.drop(key,cur);
		    this.add(key,val);}}
	    return this;}
	Ref.prototype.update=update_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 (this._inits.indexOf(fcn)<0) {
			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._id||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._id||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._id||obj.uuid||obj.oid;
	    if ((slotid)&&(val))
		fdjtState.setLocal(qid,JSON.stringify(obj));};
	OfflineKB.prototype.drop=function(obj,slotid,val){
	    var qid=obj._id||obj.uuid||obj.oid;
	    if (!(slotid)) fdjtState.dropLocal(qid);
	    else fdjtState.setLocal(qid,JSON.stringify(obj));};
	OfflineKB.prototype.Import=function(obj){
	    var qid=obj._id||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 (!(arr_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) && (!(arr_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=arr_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 (arr_position(obj[field],val)<0)
		    return false;
		else return true;}
	    else return false;};

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

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

	fdjtKB.indexOf=function(array,elt,pos){
	    if (pos) return array.indexOf(elt,pos);
	    else return array.indexOf(elt);};

	fdjtKB.contains=arr_contains;
	fdjtKB.position=arr_position;
	
	return fdjtKB;})();

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

/* 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;
    var getChild=fdjtDOM.getChild;

    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;
	if (arguments.length>4)
	    fdjtDOM.appendArray(span,arguments,4);
	return span;}
    fdjtUI.CheckSpan=CheckSpan;

    function checkable(elt){
	return (elt.nodeType===1)&&
	    (elt.tagName==='INPUT')&&
	    ((elt.type=='checkbox')||(elt.type=='radio'));}

    function checkspan_set(target,checked) {
	if (typeof target === 'string') target=fdjtID(target);
	else if (target.length) {
	    var i=0, lim=target.length;
	    while (i<lim) checkspan_set(target[i++],checked);
	    return;}
	if ((!(target))||(!(target.nodeType))) return;
	var checkspan=((hasClass(target,"checkspan"))?(target):
		       (getParent(target,".checkspan")));
	var input=getParent(target,"input");
	if (!(checkspan)) return false;
	var inputs=(getChildren(checkspan,checkable));
	if (inputs.length===0) return false;
	if (typeof checked === 'undefined') {
	    if (input) checked=input.checked;
	    else checked=(!((inputs[0]).checked));}
	if (input) input.checked=checked;
	if (checked) addClass(checkspan,"ischecked");
	else dropClass(checkspan,"ischecked");
	var i=0; var lim=inputs.length;
	while (i<lim) {
	    var cb=inputs[i++];
	    if (cb===input) continue;
	    if ((cb.checked)&&(!(checked))) cb.checked=false;
	    else if ((!(cb.checked))&&(checked)) cb.checked=true;
	    else continue;
	    var evt=document.createEvent("HTMLEvents");
	    evt.initEvent("change",false,true);
	    cb.dispatchEvent(evt);}
	return true;}
    fdjtUI.CheckSpan.set=checkspan_set;

    function checkspan_onclick(evt) {
	evt=evt||event;
	var target=evt.target||evt.srcTarget;
	if (getParent(target,"input"))
	    setTimeout(function(){checkspan_set(target);},100);
	else {
	    checkspan_set(target);
	    fdjtUI.cancel(evt);}
	return false;}
    fdjtUI.CheckSpan.onclick=checkspan_onclick;    
    })();

(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,"hidden")) {}
		    else 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)&&(hasClass(head,"hidden"))) {}
			else 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,cueclass){
	if (!(this.initialized)) initCompletions(this);
	if (!(cueclass)) cueclass="cue";
	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,cueclass)) continue;
		    addClass(c,cueclass);
		    cues.push(c);}}}
	return cues;};

    Completions.prototype.setClass=function(values,classname){
	if (!(this.initialized)) initCompletions(this);
	var drop=fdjtDOM.getChildren(this.dom,".completion."+classname);
	if ((drop)&&(drop.length))
	    dropClass(fdjtDOM.Array(drop),"hidden");
	var changed=[];
	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,classname)) continue;
		    addClass(c,classname);
		    changed.push(c);}}}
	return changed;}
    Completions.prototype.extendClass=function(values,classname){
	if (!(this.initialized)) initCompletions(this);
	var changed=[];
	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,classname)) continue;
		    addClass(c,classname);
		    changed.push(c);}}}
	return changed;};
    
    Completions.prototype.dropClass=function(classname){
	var drop=fdjtDOM.getChildren(this.dom,".completion."+classname);
	if ((drop)&&(drop.length))
	    dropClass(fdjtDOM.Array(drop),classname);};

    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(target);}
    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,"clickable"))||
		(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;};

    function submitEvent(arg){
	var form=((arg.nodeType)?(arg):(fdjtUI.T(arg)));
	while (form) {
	    if (form.tagName==='FORM') break;
	    else form=form.parentNode;}
	if (!(form)) return;
	var submit_evt = document.createEvent("HTMLEvents");
	submit_evt.initEvent("submit", true, true);
	form.dispatchEvent(submit_evt);
	return;}
    fdjtUI.submitEvent=submitEvent;

    function focusEvent(arg){
	var elt=((arg.nodeType)?(arg):(fdjtUI.T(arg)));
	var focus_evt = document.createEvent("HTMLEvents");
	focus_evt.initEvent("focus", true, true);
	elt.dispatchEvent(focus_evt);
	return;}
    fdjtUI.focusEvent=focusEvent;


}());

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

/* 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;}
	
	// This is a random nodeid used to generate UUIDs
	//  We use it because we can't access the MAC address
	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.userAgent; 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);}
	    if (result['Chrome']) result.browser='Chrome';
	    else if (result['Opera']) result.browser='Opera';
	    else if (result['Safari']) result.browser='Safari';
	    else if ((result['Safari'])&&(result['Mobile']))
		result.browser='MobileSafari';
	    else if (result['Firefox']) result.browser='Firefox';
	    else if ((result['Explorer'])||(result['IE'])||
		     (result['InternetExplorer'])||(result['MSIE']))
		result.browser='IE';
	    else if (result['Mozilla']) result.browser='Mozilla';
	    else result.browser='Browser';
	    result.platform=navigator.platform||"Turing";
	    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; -*- */

/* 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();
	    var uri=compose_uri(base_uri,args);
	    req.open("GET",uri,true);
	    req.withCredentials=true;
	    req.onreadystatechange=function () {
		if ((req.readyState == 4) && (req.status == 200)) {
		    callback(req);}};
	    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))&&
		    (((/(radio)|(checkbox)/i).exec(input.type))?
		     (input.checked):(true)))
		    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,cbctype){
	    var ajax_uri=form.getAttribute("ajaxaction")||form.action;
	    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 if (form.method==="PUT")
		req.open('PUT',ajax_uri,(!(syncp)));
	    else req.open('POST',ajax_uri,(!(syncp)));
	    if (cbctype) req.setRequestHeader("Accept",cbctype);
	    req.withCredentials=true;
	    req.onreadystatechange=function () {
		if (trace_ajax)
		    fdjtLog("Got callback (%d,%d) %o for %o, callback=%o",
			    req.readyState,req.status,req,ajax_uri,callback);
		if ((req.readyState === 4) && (req.status>=200) && (req.status<300)) {
		    if ((callback)&&(trace_ajax))
			fdjtLog("Got callback (%d,%d) %o for %o, calling %o",
				req.readyState,req.status,req,ajax_uri,callback);
		    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 {
		    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;

	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');
        };
    }
}());
/* -*- Mode: Javascript; Character-encoding: utf-8; -*- */

/* Copyright (C) 2009-2011 beingmeta, inc.
   This file is a part of the FDJT web toolkit (www.fdjt.org)
   It implements a method for breaking narrative HTML content
   across multiple pages, attempting to honor page break constraints,
   etc.

   Check out the 'mini manual' at the bottom of the file or read the
   code itself.

   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

   Use and redistribution (especially embedding in other CC licensed
   content) is also permitted under the terms of the Creative Commons
   "Attribution-NonCommercial" license:

   http://creativecommons.org/licenses/by-nc/3.0/ 

   Other uses may be allowed based on prior agreement with
   beingmeta, inc.  Inquiries can be addressed to:

   licensing@beingmeta.com

   Enjoy!

*/

/*
 * A JavaScript implementation of various hashing algorithms, merged
 *   into a single object.
 * Version 2.2 Copyright (C) Paul Johnston 1999 - 2009
 * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
 * Distributed under the BSD License
 * See http://pajhome.org.uk/crypt/md5 for more info.
 */

if (typeof fdjtHash === 'undefined')
    var fdjtHash=(function(){
	/*
	 * Configurable variables. You may need to tweak these to be compatible with
	 * the server-side, but the defaults work in most cases.
	 */
	var hexcase = 0;   /* hex output format. 0 - lowercase; 1 - uppercase        */
	var b64pad  = "";  /* base-64 pad character. "=" for strict RFC compliance   */
	var enc=false;

	function gethexcase(){ return hexcase;}
	function sethexcase(v){ hexcase=v;}
	function getpadchar(){ return b64pad;}
	function setpadchar(v){ b64pad=v;}

	function getenc(){ return ;}
	function setenc(v) {
	    if (typeof v === 'string')
		enc=fdjtHash[v]||false;
	    else enc=v;}

	/*
	 * Convert a raw string to a hex string
	 */
	function rstr2hex(input)
	{
	    try { hexcase } catch(e) { hexcase=0; }
	    var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef";
	    var output = "";
	    var x;
	    for(var i = 0; i < input.length; i++)
	    {
		x = input.charCodeAt(i);
		output += hex_tab.charAt((x >>> 4) & 0x0F)
		    +  hex_tab.charAt( x        & 0x0F);
	    }
	    return output;
	}

	/*
	 * Convert a raw string to a base-64 string
	 */
	function rstr2b64(input)
	{
	    try { b64pad } catch(e) { b64pad=''; }
	    var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
	    var output = "";
	    var len = input.length;
	    for(var i = 0; i < len; i += 3)
	    {
		var triplet = (input.charCodeAt(i) << 16)
		    | (i + 1 < len ? input.charCodeAt(i+1) << 8 : 0)
		    | (i + 2 < len ? input.charCodeAt(i+2)      : 0);
		for(var j = 0; j < 4; j++)
		{
		    if(i * 8 + j * 6 > input.length * 8) output += b64pad;
		    else output += tab.charAt((triplet >>> 6*(3-j)) & 0x3F);
		}
	    }
	    return output;
	}

	/*
	 * Convert a raw string to an arbitrary string encoding
	 */
	function rstr2any(input, encoding)
	{
	    if (!(encoding)) {
		if (enc) return enc(input);
		else return rstr2hex(input);}
	    var divisor = encoding.length;
	    var i, j, q, x, quotient;

	    /* Convert to an array of 16-bit big-endian values, forming the dividend */
	    var dividend = Array(Math.ceil(input.length / 2));
	    for(i = 0; i < dividend.length; i++)
	    {
		dividend[i] = (input.charCodeAt(i * 2) << 8) | input.charCodeAt(i * 2 + 1);
	    }

	    /*
	     * Repeatedly perform a long division. The binary array forms the dividend,
	     * the length of the encoding is the divisor. Once computed, the quotient
	     * forms the dividend for the next step. All remainders are stored for later
	     * use.
	     */
	    var full_length = Math.ceil(input.length * 8 /
					(Math.log(encoding.length) / Math.log(2)));
	    var remainders = Array(full_length);
	    for(j = 0; j < full_length; j++)
	    {
		quotient = Array();
		x = 0;
		for(i = 0; i < dividend.length; i++)
		{
		    x = (x << 16) + dividend[i];
		    q = Math.floor(x / divisor);
		    x -= q * divisor;
		    if(quotient.length > 0 || q > 0)
			quotient[quotient.length] = q;
		}
		remainders[j] = x;
		dividend = quotient;
	    }

	    /* Convert the remainders to the output string */
	    var output = "";
	    for(i = remainders.length - 1; i >= 0; i--)
		output += encoding.charAt(remainders[i]);

	    return output;
	}

	/*
	 * Encode a string as utf-8.
	 * For efficiency, this assumes the input is valid utf-16.
	 */
	function str2rstr_utf8(input)
	{
	    var output = "";
	    var i = -1;
	    var x, y;

	    while(++i < input.length)
	    {
		/* Decode utf-16 surrogate pairs */
		x = input.charCodeAt(i);
		y = i + 1 < input.length ? input.charCodeAt(i + 1) : 0;
		if(0xD800 <= x && x <= 0xDBFF && 0xDC00 <= y && y <= 0xDFFF)
		{
		    x = 0x10000 + ((x & 0x03FF) << 10) + (y & 0x03FF);
		    i++;
		}

		/* Encode output as utf-8 */
		if(x <= 0x7F)
		    output += String.fromCharCode(x);
		else if(x <= 0x7FF)
		    output += String.fromCharCode(0xC0 | ((x >>> 6 ) & 0x1F),
						  0x80 | ( x         & 0x3F));
		else if(x <= 0xFFFF)
		    output += String.fromCharCode(0xE0 | ((x >>> 12) & 0x0F),
						  0x80 | ((x >>> 6 ) & 0x3F),
						  0x80 | ( x         & 0x3F));
		else if(x <= 0x1FFFFF)
		    output += String.fromCharCode(0xF0 | ((x >>> 18) & 0x07),
						  0x80 | ((x >>> 12) & 0x3F),
						  0x80 | ((x >>> 6 ) & 0x3F),
						  0x80 | ( x         & 0x3F));
	    }
	    return output;
	}

	/*
	 * Encode a string as utf-16
	 */
	function str2rstr_utf16le(input)
	{
	    var output = "";
	    for(var i = 0; i < input.length; i++)
		output += String.fromCharCode( input.charCodeAt(i)        & 0xFF,
					       (input.charCodeAt(i) >>> 8) & 0xFF);
	    return output;
	}

	function str2rstr_utf16be(input)
	{
	    var output = "";
	    for(var i = 0; i < input.length; i++)
		output += String.fromCharCode((input.charCodeAt(i) >>> 8) & 0xFF,
					      input.charCodeAt(i)        & 0xFF);
	    return output;
	}

	/*
	 * Convert a raw string to an array of little-endian words
	 * Characters >255 have their high-byte silently ignored.
	 */
	function rstr2binl(input)
	{
	    var output = Array(input.length >> 2);
	    for(var i = 0; i < output.length; i++)
		output[i] = 0;
	    for(var i = 0; i < input.length * 8; i += 8)
		output[i>>5] |= (input.charCodeAt(i / 8) & 0xFF) << (i%32);
	    return output;
	}

	/*
	 * Convert an array of little-endian words to a string
	 */
	function binl2rstr(input)
	{
	    var output = "";
	    for(var i = 0; i < input.length * 32; i += 8)
		output += String.fromCharCode((input[i>>5] >>> (i % 32)) & 0xFF);
	    return output;
	}

	/*
	 * Add integers, wrapping at 2^32. This uses 16-bit operations internally
	 * to work around bugs in some JS interpreters.
	 */
	function safe_add(x, y)
	{
	    var lsw = (x & 0xFFFF) + (y & 0xFFFF);
	    var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
	    return (msw << 16) | (lsw & 0xFFFF);
	}

	/*
	 * Bitwise rotate a 32-bit number to the left.
	 */
	function bit_rol(num, cnt)
	{
	    return (num << cnt) | (num >>> (32 - cnt));
	}

	/*
	 * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message
	 * Digest Algorithm, as defined in RFC 1321.
	 * Version 2.2 Copyright (C) Paul Johnston 1999 - 2009
	 * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
	 * Distributed under the BSD License
	 * See http://pajhome.org.uk/crypt/md5 for more info.
	 */

	/*
	 * These are the functions you'll usually want to call
	 * They take string arguments and return either hex or base-64 encoded strings
	 */
	function hex_md5(s)    { return rstr2hex(rstr_md5(str2rstr_utf8(s))); }
	function b64_md5(s)    { return rstr2b64(rstr_md5(str2rstr_utf8(s))); }
	function any_md5(s, e) { return rstr2any(rstr_md5(str2rstr_utf8(s)), e); }
	function hex_hmac_md5(k, d)
	{ return rstr2hex(rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d))); }
	function b64_hmac_md5(k, d)
	{ return rstr2b64(rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d))); }
	function any_hmac_md5(k, d, e)
	{ return rstr2any(rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d)), e); }

	/*
	 * Perform a simple self-test to see if the VM is working
	 */
	function md5_vm_test()
	{
	    return hex_md5("abc").toLowerCase() == "900150983cd24fb0d6963f7d28e17f72";
	}

	/*
	 * Calculate the MD5 of a raw string
	 */
	function rstr_md5(s)
	{
	    return binl2rstr(binl_md5(rstr2binl(s), s.length * 8));
	}

	/*
	 * Calculate the HMAC-MD5, of a key and some data (raw strings)
	 */
	function rstr_hmac_md5(key, data)
	{
	    var bkey = rstr2binl(key);
	    if(bkey.length > 16) bkey = binl_md5(bkey, key.length * 8);

	    var ipad = Array(16), opad = Array(16);
	    for(var i = 0; i < 16; i++)
	    {
		ipad[i] = bkey[i] ^ 0x36363636;
		opad[i] = bkey[i] ^ 0x5C5C5C5C;
	    }

	    var hash = binl_md5(ipad.concat(rstr2binl(data)), 512 + data.length * 8);
	    return binl2rstr(binl_md5(opad.concat(hash), 512 + 128));
	}

	/* Calculate the MD5 of an array of little-endian words, and a bit length.
	 */
	function binl_md5(x, len)
	{
	    /* append padding */
	    x[len >> 5] |= 0x80 << ((len) % 32);
	    x[(((len + 64) >>> 9) << 4) + 14] = len;

	    var a =  1732584193;
	    var b = -271733879;
	    var c = -1732584194;
	    var d =  271733878;

	    for(var i = 0; i < x.length; i += 16)
	    {
		var olda = a;
		var oldb = b;
		var oldc = c;
		var oldd = d;

		a = md5_ff(a, b, c, d, x[i+ 0], 7 , -680876936);
		d = md5_ff(d, a, b, c, x[i+ 1], 12, -389564586);
		c = md5_ff(c, d, a, b, x[i+ 2], 17,  606105819);
		b = md5_ff(b, c, d, a, x[i+ 3], 22, -1044525330);
		a = md5_ff(a, b, c, d, x[i+ 4], 7 , -176418897);
		d = md5_ff(d, a, b, c, x[i+ 5], 12,  1200080426);
		c = md5_ff(c, d, a, b, x[i+ 6], 17, -1473231341);
		b = md5_ff(b, c, d, a, x[i+ 7], 22, -45705983);
		a = md5_ff(a, b, c, d, x[i+ 8], 7 ,  1770035416);
		d = md5_ff(d, a, b, c, x[i+ 9], 12, -1958414417);
		c = md5_ff(c, d, a, b, x[i+10], 17, -42063);
		b = md5_ff(b, c, d, a, x[i+11], 22, -1990404162);
		a = md5_ff(a, b, c, d, x[i+12], 7 ,  1804603682);
		d = md5_ff(d, a, b, c, x[i+13], 12, -40341101);
		c = md5_ff(c, d, a, b, x[i+14], 17, -1502002290);
		b = md5_ff(b, c, d, a, x[i+15], 22,  1236535329);

		a = md5_gg(a, b, c, d, x[i+ 1], 5 , -165796510);
		d = md5_gg(d, a, b, c, x[i+ 6], 9 , -1069501632);
		c = md5_gg(c, d, a, b, x[i+11], 14,  643717713);
		b = md5_gg(b, c, d, a, x[i+ 0], 20, -373897302);
		a = md5_gg(a, b, c, d, x[i+ 5], 5 , -701558691);
		d = md5_gg(d, a, b, c, x[i+10], 9 ,  38016083);
		c = md5_gg(c, d, a, b, x[i+15], 14, -660478335);
		b = md5_gg(b, c, d, a, x[i+ 4], 20, -405537848);
		a = md5_gg(a, b, c, d, x[i+ 9], 5 ,  568446438);
		d = md5_gg(d, a, b, c, x[i+14], 9 , -1019803690);
		c = md5_gg(c, d, a, b, x[i+ 3], 14, -187363961);
		b = md5_gg(b, c, d, a, x[i+ 8], 20,  1163531501);
		a = md5_gg(a, b, c, d, x[i+13], 5 , -1444681467);
		d = md5_gg(d, a, b, c, x[i+ 2], 9 , -51403784);
		c = md5_gg(c, d, a, b, x[i+ 7], 14,  1735328473);
		b = md5_gg(b, c, d, a, x[i+12], 20, -1926607734);

		a = md5_hh(a, b, c, d, x[i+ 5], 4 , -378558);
		d = md5_hh(d, a, b, c, x[i+ 8], 11, -2022574463);
		c = md5_hh(c, d, a, b, x[i+11], 16,  1839030562);
		b = md5_hh(b, c, d, a, x[i+14], 23, -35309556);
		a = md5_hh(a, b, c, d, x[i+ 1], 4 , -1530992060);
		d = md5_hh(d, a, b, c, x[i+ 4], 11,  1272893353);
		c = md5_hh(c, d, a, b, x[i+ 7], 16, -155497632);
		b = md5_hh(b, c, d, a, x[i+10], 23, -1094730640);
		a = md5_hh(a, b, c, d, x[i+13], 4 ,  681279174);
		d = md5_hh(d, a, b, c, x[i+ 0], 11, -358537222);
		c = md5_hh(c, d, a, b, x[i+ 3], 16, -722521979);
		b = md5_hh(b, c, d, a, x[i+ 6], 23,  76029189);
		a = md5_hh(a, b, c, d, x[i+ 9], 4 , -640364487);
		d = md5_hh(d, a, b, c, x[i+12], 11, -421815835);
		c = md5_hh(c, d, a, b, x[i+15], 16,  530742520);
		b = md5_hh(b, c, d, a, x[i+ 2], 23, -995338651);

		a = md5_ii(a, b, c, d, x[i+ 0], 6 , -198630844);
		d = md5_ii(d, a, b, c, x[i+ 7], 10,  1126891415);
		c = md5_ii(c, d, a, b, x[i+14], 15, -1416354905);
		b = md5_ii(b, c, d, a, x[i+ 5], 21, -57434055);
		a = md5_ii(a, b, c, d, x[i+12], 6 ,  1700485571);
		d = md5_ii(d, a, b, c, x[i+ 3], 10, -1894986606);
		c = md5_ii(c, d, a, b, x[i+10], 15, -1051523);
		b = md5_ii(b, c, d, a, x[i+ 1], 21, -2054922799);
		a = md5_ii(a, b, c, d, x[i+ 8], 6 ,  1873313359);
		d = md5_ii(d, a, b, c, x[i+15], 10, -30611744);
		c = md5_ii(c, d, a, b, x[i+ 6], 15, -1560198380);
		b = md5_ii(b, c, d, a, x[i+13], 21,  1309151649);
		a = md5_ii(a, b, c, d, x[i+ 4], 6 , -145523070);
		d = md5_ii(d, a, b, c, x[i+11], 10, -1120210379);
		c = md5_ii(c, d, a, b, x[i+ 2], 15,  718787259);
		b = md5_ii(b, c, d, a, x[i+ 9], 21, -343485551);

		a = safe_add(a, olda);
		b = safe_add(b, oldb);
		c = safe_add(c, oldc);
		d = safe_add(d, oldd);
	    }
	    return Array(a, b, c, d);
	}

	/*
	 * These functions implement the four basic operations the algorithm uses.
	 */
	function md5_cmn(q, a, b, x, s, t)
	{
	    return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s),b);
	}
	function md5_ff(a, b, c, d, x, s, t)
	{
	    return md5_cmn((b & c) | ((~b) & d), a, b, x, s, t);
	}
	function md5_gg(a, b, c, d, x, s, t)
	{
	    return md5_cmn((b & d) | (c & (~d)), a, b, x, s, t);
	}
	function md5_hh(a, b, c, d, x, s, t)
	{
	    return md5_cmn(b ^ c ^ d, a, b, x, s, t);
	}
	function md5_ii(a, b, c, d, x, s, t)
	{
	    return md5_cmn(c ^ (b | (~d)), a, b, x, s, t);
	}

	/*
	 * A JavaScript implementation of the RIPEMD-160 Algorithm
	 * Version 2.2 Copyright Jeremy Lin, Paul Johnston 2000 - 2009.
	 * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
	 * Distributed under the BSD License
	 * See http://pajhome.org.uk/crypt/md5 for details.
	 * Also http://www.ocf.berkeley.edu/~jjlin/jsotp/
	 */

	/*
	 * These are the functions you'll usually want to call
	 * They take string arguments and return either hex or base-64 encoded strings
	 */
	function hex_rmd160(s)    { return rstr2hex(rstr_rmd160(str2rstr_utf8(s))); }
	function b64_rmd160(s)    { return rstr2b64(rstr_rmd160(str2rstr_utf8(s))); }
	function any_rmd160(s, e) { return rstr2any(rstr_rmd160(str2rstr_utf8(s)), e); }
	function hex_hmac_rmd160(k, d)
	{ return rstr2hex(rstr_hmac_rmd160(str2rstr_utf8(k), str2rstr_utf8(d))); }
	function b64_hmac_rmd160(k, d)
	{ return rstr2b64(rstr_hmac_rmd160(str2rstr_utf8(k), str2rstr_utf8(d))); }
	function any_hmac_rmd160(k, d, e)
	{ return rstr2any(rstr_hmac_rmd160(str2rstr_utf8(k), str2rstr_utf8(d)), e); }

	/*
	 * Perform a simple self-test to see if the VM is working
	 */
	function rmd160_vm_test()
	{
	    return hex_rmd160("abc").toLowerCase() == "8eb208f7e05d987a9b044a8e98c6b087f15a0bfc";
	}

	/*
	 * Calculate the rmd160 of a raw string
	 */
	function rstr_rmd160(s)
	{
	    return binl2rstr(binl_rmd160(rstr2binl(s), s.length * 8));
	}

	/*
	 * Calculate the HMAC-rmd160 of a key and some data (raw strings)
	 */
	function rstr_hmac_rmd160(key, data)
	{
	    var bkey = rstr2binl(key);
	    if(bkey.length > 16) bkey = binl_rmd160(bkey, key.length * 8);

	    var ipad = Array(16), opad = Array(16);
	    for(var i = 0; i < 16; i++)
	    {
		ipad[i] = bkey[i] ^ 0x36363636;
		opad[i] = bkey[i] ^ 0x5C5C5C5C;
	    }

	    var hash = binl_rmd160(ipad.concat(rstr2binl(data)), 512 + data.length * 8);
	    return binl2rstr(binl_rmd160(opad.concat(hash), 512 + 160));
	}


	/*
	 * Calculate the RIPE-MD160 of an array of little-endian words, and a bit length.
	 */
	function binl_rmd160(x, len)
	{
	    /* append padding */
	    x[len >> 5] |= 0x80 << (len % 32);
	    x[(((len + 64) >>> 9) << 4) + 14] = len;

	    var h0 = 0x67452301;
	    var h1 = 0xefcdab89;
	    var h2 = 0x98badcfe;
	    var h3 = 0x10325476;
	    var h4 = 0xc3d2e1f0;

	    for (var i = 0; i < x.length; i += 16) {
		var T;
		var A1 = h0, B1 = h1, C1 = h2, D1 = h3, E1 = h4;
		var A2 = h0, B2 = h1, C2 = h2, D2 = h3, E2 = h4;
		for (var j = 0; j <= 79; ++j) {
		    T = safe_add(A1, rmd160_f(j, B1, C1, D1));
		    T = safe_add(T, x[i + rmd160_r1[j]]);
		    T = safe_add(T, rmd160_K1(j));
		    T = safe_add(bit_rol(T, rmd160_s1[j]), E1);
		    A1 = E1; E1 = D1; D1 = bit_rol(C1, 10); C1 = B1; B1 = T;
		    T = safe_add(A2, rmd160_f(79-j, B2, C2, D2));
		    T = safe_add(T, x[i + rmd160_r2[j]]);
		    T = safe_add(T, rmd160_K2(j));
		    T = safe_add(bit_rol(T, rmd160_s2[j]), E2);
		    A2 = E2; E2 = D2; D2 = bit_rol(C2, 10); C2 = B2; B2 = T;
		}
		T = safe_add(h1, safe_add(C1, D2));
		h1 = safe_add(h2, safe_add(D1, E2));
		h2 = safe_add(h3, safe_add(E1, A2));
		h3 = safe_add(h4, safe_add(A1, B2));
		h4 = safe_add(h0, safe_add(B1, C2));
		h0 = T;
	    }
	    return [h0, h1, h2, h3, h4];
	}

	function rmd160_f(j, x, y, z)
	{
	    return ( 0 <= j && j <= 15) ? (x ^ y ^ z) :
		(16 <= j && j <= 31) ? (x & y) | (~x & z) :
		(32 <= j && j <= 47) ? (x | ~y) ^ z :
		(48 <= j && j <= 63) ? (x & z) | (y & ~z) :
		(64 <= j && j <= 79) ? x ^ (y | ~z) :
		"rmd160_f: j out of range";
	}
	function rmd160_K1(j)
	{
	    return ( 0 <= j && j <= 15) ? 0x00000000 :
		(16 <= j && j <= 31) ? 0x5a827999 :
		(32 <= j && j <= 47) ? 0x6ed9eba1 :
		(48 <= j && j <= 63) ? 0x8f1bbcdc :
		(64 <= j && j <= 79) ? 0xa953fd4e :
		"rmd160_K1: j out of range";
	}
	function rmd160_K2(j)
	{
	    return ( 0 <= j && j <= 15) ? 0x50a28be6 :
		(16 <= j && j <= 31) ? 0x5c4dd124 :
		(32 <= j && j <= 47) ? 0x6d703ef3 :
		(48 <= j && j <= 63) ? 0x7a6d76e9 :
		(64 <= j && j <= 79) ? 0x00000000 :
		"rmd160_K2: j out of range";
	}
	var rmd160_r1 = [
	    0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15,
	    7,  4, 13,  1, 10,  6, 15,  3, 12,  0,  9,  5,  2, 14, 11,  8,
	    3, 10, 14,  4,  9, 15,  8,  1,  2,  7,  0,  6, 13, 11,  5, 12,
	    1,  9, 11, 10,  0,  8, 12,  4, 13,  3,  7, 15, 14,  5,  6,  2,
	    4,  0,  5,  9,  7, 12,  2, 10, 14,  1,  3,  8, 11,  6, 15, 13
	];
	var rmd160_r2 = [
	    5, 14,  7,  0,  9,  2, 11,  4, 13,  6, 15,  8,  1, 10,  3, 12,
	    6, 11,  3,  7,  0, 13,  5, 10, 14, 15,  8, 12,  4,  9,  1,  2,
	    15,  5,  1,  3,  7, 14,  6,  9, 11,  8, 12,  2, 10,  0,  4, 13,
	    8,  6,  4,  1,  3, 11, 15,  0,  5, 12,  2, 13,  9,  7, 10, 14,
	    12, 15, 10,  4,  1,  5,  8,  7,  6,  2, 13, 14,  0,  3,  9, 11
	];
	var rmd160_s1 = [
	    11, 14, 15, 12,  5,  8,  7,  9, 11, 13, 14, 15,  6,  7,  9,  8,
	    7,  6,  8, 13, 11,  9,  7, 15,  7, 12, 15,  9, 11,  7, 13, 12,
	    11, 13,  6,  7, 14,  9, 13, 15, 14,  8, 13,  6,  5, 12,  7,  5,
	    11, 12, 14, 15, 14, 15,  9,  8,  9, 14,  5,  6,  8,  6,  5, 12,
	    9, 15,  5, 11,  6,  8, 13, 12,  5, 12, 13, 14, 11,  8,  5,  6
	];
	var rmd160_s2 = [
	    8,  9,  9, 11, 13, 15, 15,  5,  7,  7,  8, 11, 14, 14, 12,  6,
	    9, 13, 15,  7, 12,  8,  9, 11,  7,  7, 12,  7,  6, 15, 13, 11,
	    9,  7, 15, 11,  8,  6,  6, 14, 12, 13,  5, 14, 13, 13,  7,  5,
	    15,  5,  8, 11, 14, 14,  6, 14,  6,  9, 12,  9, 12,  5, 15,  8,
	    8,  5, 12,  9, 12,  5, 14,  6,  8, 13,  6,  5, 15, 13, 11, 11
	];

	/*
	 * A JavaScript implementation of the Secure Hash Algorithm, SHA-1, as defined
	 * in FIPS 180-1
	 * Version 2.2 Copyright Paul Johnston 2000 - 2009.
	 * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
	 * Distributed under the BSD License
	 * See http://pajhome.org.uk/crypt/md5 for details.
	 */

	/*
	 * These are the functions you'll usually want to call
	 * They take string arguments and return either hex or base-64 encoded strings
	 */
	function hex_sha1(s)    { return rstr2hex(rstr_sha1(str2rstr_utf8(s))); }
	function b64_sha1(s)    { return rstr2b64(rstr_sha1(str2rstr_utf8(s))); }
	function any_sha1(s, e) { return rstr2any(rstr_sha1(str2rstr_utf8(s)), e); }
	function hex_hmac_sha1(k, d)
	{ return rstr2hex(rstr_hmac_sha1(str2rstr_utf8(k), str2rstr_utf8(d))); }
	function b64_hmac_sha1(k, d)
	{ return rstr2b64(rstr_hmac_sha1(str2rstr_utf8(k), str2rstr_utf8(d))); }
	function any_hmac_sha1(k, d, e)
	{ return rstr2any(rstr_hmac_sha1(str2rstr_utf8(k), str2rstr_utf8(d)), e); }

	/*
	 * Perform a simple self-test to see if the VM is working
	 */
	function sha1_vm_test()
	{
	    return hex_sha1("abc").toLowerCase() == "a9993e364706816aba3e25717850c26c9cd0d89d";
	}

	/*
	 * Calculate the SHA1 of a raw string
	 */
	function rstr_sha1(s)
	{
	    return binb2rstr(binb_sha1(rstr2binb(s), s.length * 8));
	}

	/*
	 * Calculate the HMAC-SHA1 of a key and some data (raw strings)
	 */
	function rstr_hmac_sha1(key, data)
	{
	    var bkey = rstr2binb(key);
	    if(bkey.length > 16) bkey = binb_sha1(bkey, key.length * 8);

	    var ipad = Array(16), opad = Array(16);
	    for(var i = 0; i < 16; i++)
	    {
		ipad[i] = bkey[i] ^ 0x36363636;
		opad[i] = bkey[i] ^ 0x5C5C5C5C;
	    }

	    var hash = binb_sha1(ipad.concat(rstr2binb(data)), 512 + data.length * 8);
	    return binb2rstr(binb_sha1(opad.concat(hash), 512 + 160));
	}

	/*
	 * Calculate the SHA-1 of an array of big-endian words, and a bit length
	 */
	function binb_sha1(x, len)
	{
	    /* append padding */
	    x[len >> 5] |= 0x80 << (24 - len % 32);
	    x[((len + 64 >> 9) << 4) + 15] = len;

	    var w = Array(80);
	    var a =  1732584193;
	    var b = -271733879;
	    var c = -1732584194;
	    var d =  271733878;
	    var e = -1009589776;

	    for(var i = 0; i < x.length; i += 16)
	    {
		var olda = a;
		var oldb = b;
		var oldc = c;
		var oldd = d;
		var olde = e;

		for(var j = 0; j < 80; j++)
		{
		    if(j < 16) w[j] = x[i + j];
		    else w[j] = bit_rol(w[j-3] ^ w[j-8] ^ w[j-14] ^ w[j-16], 1);
		    var t = safe_add(safe_add(bit_rol(a, 5), sha1_ft(j, b, c, d)),
				     safe_add(safe_add(e, w[j]), sha1_kt(j)));
		    e = d;
		    d = c;
		    c = bit_rol(b, 30);
		    b = a;
		    a = t;
		}

		a = safe_add(a, olda);
		b = safe_add(b, oldb);
		c = safe_add(c, oldc);
		d = safe_add(d, oldd);
		e = safe_add(e, olde);
	    }
	    return Array(a, b, c, d, e);

	}

	/*
	 * Perform the appropriate triplet combination function for the current
	 * iteration
	 */
	function sha1_ft(t, b, c, d)
	{
	    if(t < 20) return (b & c) | ((~b) & d);
	    if(t < 40) return b ^ c ^ d;
	    if(t < 60) return (b & c) | (b & d) | (c & d);
	    return b ^ c ^ d;
	}

	/*
	 * Determine the appropriate additive constant for the current iteration
	 */
	function sha1_kt(t)
	{
	    return (t < 20) ?  1518500249 : (t < 40) ?  1859775393 :
		(t < 60) ? -1894007588 : -899497514;
	}

	/*
	 * A JavaScript implementation of the Secure Hash Algorithm, SHA-256, as defined
	 * in FIPS 180-2
	 * Version 2.2 Copyright Angel Marin, Paul Johnston 2000 - 2009.
	 * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
	 * Distributed under the BSD License
	 * See http://pajhome.org.uk/crypt/md5 for details.
	 * Also http://anmar.eu.org/projects/jssha2/
	 */

	/*
	 * These are the functions you'll usually want to call
	 * They take string arguments and return either hex or base-64 encoded strings
	 */
	function hex_sha256(s)    { return rstr2hex(rstr_sha256(str2rstr_utf8(s))); }
	function b64_sha256(s)    { return rstr2b64(rstr_sha256(str2rstr_utf8(s))); }
	function any_sha256(s, e) { return rstr2any(rstr_sha256(str2rstr_utf8(s)), e); }
	function hex_hmac_sha256(k, d)
	{ return rstr2hex(rstr_hmac_sha256(str2rstr_utf8(k), str2rstr_utf8(d))); }
	function b64_hmac_sha256(k, d)
	{ return rstr2b64(rstr_hmac_sha256(str2rstr_utf8(k), str2rstr_utf8(d))); }
	function any_hmac_sha256(k, d, e)
	{ return rstr2any(rstr_hmac_sha256(str2rstr_utf8(k), str2rstr_utf8(d)), e); }

	/*
	 * Perform a simple self-test to see if the VM is working
	 */
	function sha256_vm_test()
	{
	    return hex_sha256("abc").toLowerCase() ==
		"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad";
	}

	/*
	 * Calculate the sha256 of a raw string
	 */
	function rstr_sha256(s)
	{
	    return binb2rstr(binb_sha256(rstr2binb(s), s.length * 8));
	}

	/*
	 * Calculate the HMAC-sha256 of a key and some data (raw strings)
	 */
	function rstr_hmac_sha256(key, data)
	{
	    var bkey = rstr2binb(key);
	    if(bkey.length > 16) bkey = binb_sha256(bkey, key.length * 8);

	    var ipad = Array(16), opad = Array(16);
	    for(var i = 0; i < 16; i++)
	    {
		ipad[i] = bkey[i] ^ 0x36363636;
		opad[i] = bkey[i] ^ 0x5C5C5C5C;
	    }

	    var hash = binb_sha256(ipad.concat(rstr2binb(data)), 512 + data.length * 8);
	    return binb2rstr(binb_sha256(opad.concat(hash), 512 + 256));
	}

	/*
	 * Main sha256 function, with its support functions
	 */
	function sha256_S (X, n) {return ( X >>> n ) | (X << (32 - n));}
	function sha256_R (X, n) {return ( X >>> n );}
	function sha256_Ch(x, y, z) {return ((x & y) ^ ((~x) & z));}
	function sha256_Maj(x, y, z) {return ((x & y) ^ (x & z) ^ (y & z));}
	function sha256_Sigma0256(x) {return (sha256_S(x, 2) ^ sha256_S(x, 13) ^ sha256_S(x, 22));}
	function sha256_Sigma1256(x) {return (sha256_S(x, 6) ^ sha256_S(x, 11) ^ sha256_S(x, 25));}
	function sha256_Gamma0256(x) {return (sha256_S(x, 7) ^ sha256_S(x, 18) ^ sha256_R(x, 3));}
	function sha256_Gamma1256(x) {return (sha256_S(x, 17) ^ sha256_S(x, 19) ^ sha256_R(x, 10));}
	function sha256_Sigma0512(x) {return (sha256_S(x, 28) ^ sha256_S(x, 34) ^ sha256_S(x, 39));}
	function sha256_Sigma1512(x) {return (sha256_S(x, 14) ^ sha256_S(x, 18) ^ sha256_S(x, 41));}
	function sha256_Gamma0512(x) {return (sha256_S(x, 1)  ^ sha256_S(x, 8) ^ sha256_R(x, 7));}
	function sha256_Gamma1512(x) {return (sha256_S(x, 19) ^ sha256_S(x, 61) ^ sha256_R(x, 6));}

	var sha256_K = new Array
	(
	    1116352408, 1899447441, -1245643825, -373957723, 961987163, 1508970993,
		-1841331548, -1424204075, -670586216, 310598401, 607225278, 1426881987,
	    1925078388, -2132889090, -1680079193, -1046744716, -459576895, -272742522,
	    264347078, 604807628, 770255983, 1249150122, 1555081692, 1996064986,
		-1740746414, -1473132947, -1341970488, -1084653625, -958395405, -710438585,
	    113926993, 338241895, 666307205, 773529912, 1294757372, 1396182291,
	    1695183700, 1986661051, -2117940946, -1838011259, -1564481375, -1474664885,
		-1035236496, -949202525, -778901479, -694614492, -200395387, 275423344,
	    430227734, 506948616, 659060556, 883997877, 958139571, 1322822218,
	    1537002063, 1747873779, 1955562222, 2024104815, -2067236844, -1933114872,
		-1866530822, -1538233109, -1090935817, -965641998
	);

	function binb_sha256(m, l)
	{
	    var HASH = new Array(1779033703, -1150833019, 1013904242, -1521486534,
				 1359893119, -1694144372, 528734635, 1541459225);
	    var W = new Array(64);
	    var a, b, c, d, e, f, g, h;
	    var i, j, T1, T2;

	    /* append padding */
	    m[l >> 5] |= 0x80 << (24 - l % 32);
	    m[((l + 64 >> 9) << 4) + 15] = l;

	    for(i = 0; i < m.length; i += 16)
	    {
		a = HASH[0];
		b = HASH[1];
		c = HASH[2];
		d = HASH[3];
		e = HASH[4];
		f = HASH[5];
		g = HASH[6];
		h = HASH[7];

		for(j = 0; j < 64; j++)
		{
		    if (j < 16) W[j] = m[j + i];
		    else W[j] = safe_add(safe_add(safe_add(sha256_Gamma1256(W[j - 2]), W[j - 7]),
						  sha256_Gamma0256(W[j - 15])), W[j - 16]);

		    T1 = safe_add(safe_add(safe_add(safe_add(h, sha256_Sigma1256(e)), sha256_Ch(e, f, g)),
                                           sha256_K[j]), W[j]);
		    T2 = safe_add(sha256_Sigma0256(a), sha256_Maj(a, b, c));
		    h = g;
		    g = f;
		    f = e;
		    e = safe_add(d, T1);
		    d = c;
		    c = b;
		    b = a;
		    a = safe_add(T1, T2);
		}

		HASH[0] = safe_add(a, HASH[0]);
		HASH[1] = safe_add(b, HASH[1]);
		HASH[2] = safe_add(c, HASH[2]);
		HASH[3] = safe_add(d, HASH[3]);
		HASH[4] = safe_add(e, HASH[4]);
		HASH[5] = safe_add(f, HASH[5]);
		HASH[6] = safe_add(g, HASH[6]);
		HASH[7] = safe_add(h, HASH[7]);
	    }
	    return HASH;
	}

	/*
	 * A JavaScript implementation of the Secure Hash Algorithm, SHA-512, as defined
	 * in FIPS 180-2
	 * Version 2.2 Copyright Anonymous Contributor, Paul Johnston 2000 - 2009.
	 * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
	 * Distributed under the BSD License
	 * See http://pajhome.org.uk/crypt/md5 for details.
	 */

	/*
	 * These are the functions you'll usually want to call
	 * They take string arguments and return either hex or base-64 encoded strings
	 */
	function hex_sha512(s)    { return rstr2hex(rstr_sha512(str2rstr_utf8(s))); }
	function b64_sha512(s)    { return rstr2b64(rstr_sha512(str2rstr_utf8(s))); }
	function any_sha512(s, e) { return rstr2any(rstr_sha512(str2rstr_utf8(s)), e);}
	function hex_hmac_sha512(k, d)
	{ return rstr2hex(rstr_hmac_sha512(str2rstr_utf8(k), str2rstr_utf8(d))); }
	function b64_hmac_sha512(k, d)
	{ return rstr2b64(rstr_hmac_sha512(str2rstr_utf8(k), str2rstr_utf8(d))); }
	function any_hmac_sha512(k, d, e)
	{ return rstr2any(rstr_hmac_sha512(str2rstr_utf8(k), str2rstr_utf8(d)), e);}

	/*
	 * Perform a simple self-test to see if the VM is working
	 */
	function sha512_vm_test()
	{
	    return hex_sha512("abc").toLowerCase() ==
		"ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a" +
		"2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f";
	}

	/*
	 * Calculate the SHA-512 of a raw string
	 */
	function rstr_sha512(s)
	{
	    return binb2rstr(binb_sha512(rstr2binb(s), s.length * 8));
	}

	/*
	 * Calculate the HMAC-SHA-512 of a key and some data (raw strings)
	 */
	function rstr_hmac_sha512(key, data)
	{
	    var bkey = rstr2binb(key);
	    if(bkey.length > 32) bkey = binb_sha512(bkey, key.length * 8);

	    var ipad = Array(32), opad = Array(32);
	    for(var i = 0; i < 32; i++)
	    {
		ipad[i] = bkey[i] ^ 0x36363636;
		opad[i] = bkey[i] ^ 0x5C5C5C5C;
	    }

	    var hash = binb_sha512(ipad.concat(rstr2binb(data)), 1024 + data.length * 8);
	    return binb2rstr(binb_sha512(opad.concat(hash), 1024 + 512));
	}

	/*
	 * Calculate the SHA-512 of an array of big-endian dwords, and a bit length
	 */
	var sha512_k;
	function binb_sha512(x, len)
	{
	    if(sha512_k == undefined)
	    {
		//SHA512 constants
		sha512_k = new Array(
		    new int64(0x428a2f98, -685199838), new int64(0x71374491, 0x23ef65cd),
		    new int64(-1245643825, -330482897), new int64(-373957723, -2121671748),
		    new int64(0x3956c25b, -213338824), new int64(0x59f111f1, -1241133031),
		    new int64(-1841331548, -1357295717), new int64(-1424204075, -630357736),
		    new int64(-670586216, -1560083902), new int64(0x12835b01, 0x45706fbe),
		    new int64(0x243185be, 0x4ee4b28c), new int64(0x550c7dc3, -704662302),
		    new int64(0x72be5d74, -226784913), new int64(-2132889090, 0x3b1696b1),
		    new int64(-1680079193, 0x25c71235), new int64(-1046744716, -815192428),
		    new int64(-459576895, -1628353838), new int64(-272742522, 0x384f25e3),
		    new int64(0xfc19dc6, -1953704523), new int64(0x240ca1cc, 0x77ac9c65),
		    new int64(0x2de92c6f, 0x592b0275), new int64(0x4a7484aa, 0x6ea6e483),
		    new int64(0x5cb0a9dc, -1119749164), new int64(0x76f988da, -2096016459),
		    new int64(-1740746414, -295247957), new int64(-1473132947, 0x2db43210),
		    new int64(-1341970488, -1728372417), new int64(-1084653625, -1091629340),
		    new int64(-958395405, 0x3da88fc2), new int64(-710438585, -1828018395),
		    new int64(0x6ca6351, -536640913), new int64(0x14292967, 0xa0e6e70),
		    new int64(0x27b70a85, 0x46d22ffc), new int64(0x2e1b2138, 0x5c26c926),
		    new int64(0x4d2c6dfc, 0x5ac42aed), new int64(0x53380d13, -1651133473),
		    new int64(0x650a7354, -1951439906), new int64(0x766a0abb, 0x3c77b2a8),
		    new int64(-2117940946, 0x47edaee6), new int64(-1838011259, 0x1482353b),
		    new int64(-1564481375, 0x4cf10364), new int64(-1474664885, -1136513023),
		    new int64(-1035236496, -789014639), new int64(-949202525, 0x654be30),
		    new int64(-778901479, -688958952), new int64(-694614492, 0x5565a910),
		    new int64(-200395387, 0x5771202a), new int64(0x106aa070, 0x32bbd1b8),
		    new int64(0x19a4c116, -1194143544), new int64(0x1e376c08, 0x5141ab53),
		    new int64(0x2748774c, -544281703), new int64(0x34b0bcb5, -509917016),
		    new int64(0x391c0cb3, -976659869), new int64(0x4ed8aa4a, -482243893),
		    new int64(0x5b9cca4f, 0x7763e373), new int64(0x682e6ff3, -692930397),
		    new int64(0x748f82ee, 0x5defb2fc), new int64(0x78a5636f, 0x43172f60),
		    new int64(-2067236844, -1578062990), new int64(-1933114872, 0x1a6439ec),
		    new int64(-1866530822, 0x23631e28), new int64(-1538233109, -561857047),
		    new int64(-1090935817, -1295615723), new int64(-965641998, -479046869),
		    new int64(-903397682, -366583396), new int64(-779700025, 0x21c0c207),
		    new int64(-354779690, -840897762), new int64(-176337025, -294727304),
		    new int64(0x6f067aa, 0x72176fba), new int64(0xa637dc5, -1563912026),
		    new int64(0x113f9804, -1090974290), new int64(0x1b710b35, 0x131c471b),
		    new int64(0x28db77f5, 0x23047d84), new int64(0x32caab7b, 0x40c72493),
		    new int64(0x3c9ebe0a, 0x15c9bebc), new int64(0x431d67c4, -1676669620),
		    new int64(0x4cc5d4be, -885112138), new int64(0x597f299c, -60457430),
		    new int64(0x5fcb6fab, 0x3ad6faec), new int64(0x6c44198c, 0x4a475817));
	    }

	    //Initial hash values
	    var H = new Array(
		new int64(0x6a09e667, -205731576),
		new int64(-1150833019, -2067093701),
		new int64(0x3c6ef372, -23791573),
		new int64(-1521486534, 0x5f1d36f1),
		new int64(0x510e527f, -1377402159),
		new int64(-1694144372, 0x2b3e6c1f),
		new int64(0x1f83d9ab, -79577749),
		new int64(0x5be0cd19, 0x137e2179));

	    var T1 = new int64(0, 0),
	    T2 = new int64(0, 0),
	    a = new int64(0,0),
	    b = new int64(0,0),
	    c = new int64(0,0),
	    d = new int64(0,0),
	    e = new int64(0,0),
	    f = new int64(0,0),
	    g = new int64(0,0),
	    h = new int64(0,0),
	    //Temporary variables not specified by the document
	    s0 = new int64(0, 0),
	    s1 = new int64(0, 0),
	    Ch = new int64(0, 0),
	    Maj = new int64(0, 0),
	    r1 = new int64(0, 0),
	    r2 = new int64(0, 0),
	    r3 = new int64(0, 0);
	    var j, i;
	    var W = new Array(80);
	    for(i=0; i<80; i++)
		W[i] = new int64(0, 0);

	    // append padding to the source string. The format is described in the FIPS.
	    x[len >> 5] |= 0x80 << (24 - (len & 0x1f));
	    x[((len + 128 >> 10)<< 5) + 31] = len;

	    for(i = 0; i<x.length; i+=32) //32 dwords is the block size
	    {
		int64copy(a, H[0]);
		int64copy(b, H[1]);
		int64copy(c, H[2]);
		int64copy(d, H[3]);
		int64copy(e, H[4]);
		int64copy(f, H[5]);
		int64copy(g, H[6]);
		int64copy(h, H[7]);

		for(j=0; j<16; j++)
		{
		    W[j].h = x[i + 2*j];
		    W[j].l = x[i + 2*j + 1];
		}

		for(j=16; j<80; j++)
		{
		    //sigma1
		    int64rrot(r1, W[j-2], 19);
		    int64revrrot(r2, W[j-2], 29);
		    int64shr(r3, W[j-2], 6);
		    s1.l = r1.l ^ r2.l ^ r3.l;
		    s1.h = r1.h ^ r2.h ^ r3.h;
		    //sigma0
		    int64rrot(r1, W[j-15], 1);
		    int64rrot(r2, W[j-15], 8);
		    int64shr(r3, W[j-15], 7);
		    s0.l = r1.l ^ r2.l ^ r3.l;
		    s0.h = r1.h ^ r2.h ^ r3.h;

		    int64add4(W[j], s1, W[j-7], s0, W[j-16]);
		}

		for(j = 0; j < 80; j++)
		{
		    //Ch
		    Ch.l = (e.l & f.l) ^ (~e.l & g.l);
		    Ch.h = (e.h & f.h) ^ (~e.h & g.h);

		    //Sigma1
		    int64rrot(r1, e, 14);
		    int64rrot(r2, e, 18);
		    int64revrrot(r3, e, 9);
		    s1.l = r1.l ^ r2.l ^ r3.l;
		    s1.h = r1.h ^ r2.h ^ r3.h;

		    //Sigma0
		    int64rrot(r1, a, 28);
		    int64revrrot(r2, a, 2);
		    int64revrrot(r3, a, 7);
		    s0.l = r1.l ^ r2.l ^ r3.l;
		    s0.h = r1.h ^ r2.h ^ r3.h;

		    //Maj
		    Maj.l = (a.l & b.l) ^ (a.l & c.l) ^ (b.l & c.l);
		    Maj.h = (a.h & b.h) ^ (a.h & c.h) ^ (b.h & c.h);

		    int64add5(T1, h, s1, Ch, sha512_k[j], W[j]);
		    int64add(T2, s0, Maj);

		    int64copy(h, g);
		    int64copy(g, f);
		    int64copy(f, e);
		    int64add(e, d, T1);
		    int64copy(d, c);
		    int64copy(c, b);
		    int64copy(b, a);
		    int64add(a, T1, T2);
		}
		int64add(H[0], H[0], a);
		int64add(H[1], H[1], b);
		int64add(H[2], H[2], c);
		int64add(H[3], H[3], d);
		int64add(H[4], H[4], e);
		int64add(H[5], H[5], f);
		int64add(H[6], H[6], g);
		int64add(H[7], H[7], h);
	    }

	    //represent the hash as an array of 32-bit dwords
	    var hash = new Array(16);
	    for(i=0; i<8; i++)
	    {
		hash[2*i] = H[i].h;
		hash[2*i + 1] = H[i].l;
	    }
	    return hash;
	}

	//A constructor for 64-bit numbers
	function int64(h, l)
	{
	    this.h = h;
	    this.l = l;
	    //this.toString = int64toString;
	}

	//Copies src into dst, assuming both are 64-bit numbers
	function int64copy(dst, src)
	{
	    dst.h = src.h;
	    dst.l = src.l;
	}

	//Right-rotates a 64-bit number by shift
	//Won't handle cases of shift>=32
	//The function revrrot() is for that
	function int64rrot(dst, x, shift)
	{
	    dst.l = (x.l >>> shift) | (x.h << (32-shift));
	    dst.h = (x.h >>> shift) | (x.l << (32-shift));
	}

	//Reverses the dwords of the source and then rotates right by shift.
	//This is equivalent to rotation by 32+shift
	function int64revrrot(dst, x, shift)
	{
	    dst.l = (x.h >>> shift) | (x.l << (32-shift));
	    dst.h = (x.l >>> shift) | (x.h << (32-shift));
	}

	//Bitwise-shifts right a 64-bit number by shift
	//Won't handle shift>=32, but it's never needed in SHA512
	function int64shr(dst, x, shift)
	{
	    dst.l = (x.l >>> shift) | (x.h << (32-shift));
	    dst.h = (x.h >>> shift);
	}

	//Adds two 64-bit numbers
	//Like the original implementation, does not rely on 32-bit operations
	function int64add(dst, x, y)
	{
	    var w0 = (x.l & 0xffff) + (y.l & 0xffff);
	    var w1 = (x.l >>> 16) + (y.l >>> 16) + (w0 >>> 16);
	    var w2 = (x.h & 0xffff) + (y.h & 0xffff) + (w1 >>> 16);
	    var w3 = (x.h >>> 16) + (y.h >>> 16) + (w2 >>> 16);
	    dst.l = (w0 & 0xffff) | (w1 << 16);
	    dst.h = (w2 & 0xffff) | (w3 << 16);
	}

	//Same, except with 4 addends. Works faster than adding them one by one.
	function int64add4(dst, a, b, c, d)
	{
	    var w0 = (a.l & 0xffff) + (b.l & 0xffff) + (c.l & 0xffff) + (d.l & 0xffff);
	    var w1 = (a.l >>> 16) + (b.l >>> 16) + (c.l >>> 16) + (d.l >>> 16) + (w0 >>> 16);
	    var w2 = (a.h & 0xffff) + (b.h & 0xffff) + (c.h & 0xffff) + (d.h & 0xffff) + (w1 >>> 16);
	    var w3 = (a.h >>> 16) + (b.h >>> 16) + (c.h >>> 16) + (d.h >>> 16) + (w2 >>> 16);
	    dst.l = (w0 & 0xffff) | (w1 << 16);
	    dst.h = (w2 & 0xffff) | (w3 << 16);
	}

	//Same, except with 5 addends
	function int64add5(dst, a, b, c, d, e)
	{
	    var w0 = (a.l & 0xffff) + (b.l & 0xffff) + (c.l & 0xffff) + (d.l & 0xffff) + (e.l & 0xffff);
	    var w1 = (a.l >>> 16) + (b.l >>> 16) + (c.l >>> 16) + (d.l >>> 16) + (e.l >>> 16) + (w0 >>> 16);
	    var w2 = (a.h & 0xffff) + (b.h & 0xffff) + (c.h & 0xffff) + (d.h & 0xffff) + (e.h & 0xffff) + (w1 >>> 16);
	    var w3 = (a.h >>> 16) + (b.h >>> 16) + (c.h >>> 16) + (d.h >>> 16) + (e.h >>> 16) + (w2 >>> 16);
	    dst.l = (w0 & 0xffff) | (w1 << 16);
	    dst.h = (w2 & 0xffff) | (w3 << 16);
	}

	return {
	    "_get_hexcase_": gethexcase, "_set_hexcase_": sethexcase,
	    "_get_padchar_": getpadchar, "_set_padchar_": setpadchar,
	    "_get_outenc_": getenc, "_set_outenc_": setenc,
	    "hex": rstr2hex, b64: "rst2b64", base64: "rst2b64",
	    "array": rstr2binl, "binl": rstr2binl,
	    hex_md5: hex_md5,
	    b64_md5: b64_md5,
	    md5: any_md5,
	    hex_hmac_md5: hex_hmac_md5,
	    b64_hmac_md5: b64_hmac_md5,
	    hmac_md5: any_hmac_md5,
	    hex_rmd160: hex_rmd160,
	    b64_rmd160: b64_rmd160,
	    rmd160: any_rmd160,
	    hex_hmac_rmd160: hex_hmac_rmd160,
	    b64_hmac_rmd160: b64_hmac_rmd160,
	    hmac_rmd160: any_hmac_rmd160,
	    hex_sha1: hex_sha1,
	    b64_sha1: b64_sha1,
	    sha1: any_sha1,
	    hex_hmac_sha1: hex_hmac_sha1,
	    b64_hmac_sha1: b64_hmac_sha1,
	    hmac_sha1: any_hmac_sha1,
	    hex_sha256: hex_sha256,
	    b64_sha256: b64_sha256,
	    sha256: any_sha256,
	    hex_hmac_sha256: hex_hmac_sha256,
	    b64_hmac_sha256: b64_hmac_sha256,
	    hmac_sha256: any_hmac_sha256,
	    hex_sha512: hex_sha512,
	    b64_sha512: b64_sha512,
	    sha512: any_sha512,
	    hex_hmac_sha512: hex_hmac_sha512,
	    b64_hmac_sha512: b64_hmac_sha512,
	    hmac_sha512: any_hmac_sha512};

    })();
/* -*- Mode: Javascript; Character-encoding: utf-8; -*- */

/* Copyright (C) 2011 beingmeta, inc.
   This file is a part of the FDJT web toolkit (www.fdjt.org)
   It implements a method for breaking narrative HTML content
   across multiple pages, attempting to honor page break constraints,
   etc.

   Check out the 'mini manual' at the bottom of the file or read the
   code itself.

   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

   Use and redistribution (especially embedding in other CC licensed
   content) is also permitted under the terms of the Creative Commons
   "Attribution-NonCommercial" license:

   http://creativecommons.org/licenses/by-nc/3.0/ 

   Other uses may be allowed based on prior agreement with
   beingmeta, inc.  Inquiries can be addressed to:

   licensing@beingmeta.com

   Enjoy!

*/

var WSN=(function(){

    var unicode_regex=/(\p{Mark})/g;
    
    function WSN(arg,sortfn,wordfn,keepdup){
	if (!(arg)) {
	    // Assume we're being used as a constructor.
	    if (sortfn) this.sortfn=sortfn;
	    if (wordfn) this.wordfn=wordfn;
	    if (keepdup) this.keepdup=keepdup;
	    return this;}
	if (typeof sortfn === 'undefined') sortfn=WSN.sortfn||false;
	if (typeof wordfn === 'undefined') wordfn=WSN.wordfn||false;
	if (typeof keepdup === 'undefined') keepdup=WSN.keepdup||false;
	if (typeof arg === 'string') {
	    var norm=
		((unicode_regex)?
		 (arg.toLowerCase().replace(unicode_regex,"")):
		 (arg.toLowerCase()));
	    if (norm.search(/\S/)>0)
		norm=norm.slice(norm.search(/\S/));
	    var words=norm.split(/\W*\s+\W*/g);
	    var nwords=words.length;
	    if (nwords===0) return "";
	    else words[0]=words[0].replace(/^\W+/,"");
	    if (nwords>1)
		words[nwords-1]=words[nwords-1].replace(/\W+$/,"");
	    if (wordfn) {
		if (typeof wordfn === 'number') {
		    var nwords=[];
		    var i=0; var lim=words.length;
		    while (i<lim) {
			var word=words[i++];
			if (word.length>wordfn) nwords.push(word);}
		    if (nwords.length) words=nwords;}
		else if (wordfn.call) {
		    var nwords=[];
		    var i=0; var lim=words.length;
		    while (i<lim) {
			var nword=wordfn(words[i++]);
			if (nword) nwords.push(nword);
			i++;}
		    if (nwords.length) words=nwords;}
		else  {
		    var nwords=[];
		    var i=0; var lim=words.length;
		    while (i<lim) {
			var word=words[i++];
			var nword=wordfn[word];
			if (nword==="") {}
			else if ((!(nword))||(typeof nword !== 'string'))
			    nwords.push(word);
			else nwords.push(nword);}
		    if (nwords.length) words=nwords;}}
	    var sorter=sortfn;
	    // By default, use lensort
	    // But if you're passed nativesort, just
	    //  pass false to sort()
	    if (sortfn===true) sorter=lensort;
	    else if (sortfn===nativesort) sorter=false;
	    else {}
	    if ((sortfn)&&(keepdup))
		return words.sort(sorter).join(" ");
	    else if (sortfn)
		return dedupfn(words.sort(sorter)).join(" ");
	    else return words.join(" ");}
	else if (!(arg.nodeType))
	    throw new Exception("bad arg to WSN");
	else if (arg.nodeType===3)
	    return WSN(arg.nodeValue);
	else if (arg.nodeType===1)
	    return WSN(textify(arg));
	else throw new Exception("bad arg to WSN");}
    
    function dedupfn(arr){
	var i=0; var lim=arr.length; var last=false;
	if (lim<2) return arr;
	else while (i<lim) {
	    if ((last)&&(arr[i]===last)) return dodedup(arr);
	    else last=arr[i++];}
	return arr;}
    function dodedup(arr){
	var last=arr[0]; var result=[last];
	var i=1; var lim=arr.length;
	while (i<lim) 
	    if (arr[i]===last) i++;
	    else result.push(last=arr[i++]);
	return result;}
    
    function lensort(x,y){
	var xl=x.length, yl=y.length;
	if (xl===yl) {
	    if (x>y) return -1;
	    else if (x<y) return 1;
	    else return 0;}
	else if (xl>yl) return -1;
	else return 1;}
    WSN.lensort=lensort;
    function nativesort(x,y){
	if (x>y) return -1;
	else if (x<y) return 1;
	else return 0;}
    WSN.nativesort=nativesort;

    function textify(arg,text){
	if (!(arg.nodeType)) return text||"";
	else if (arg.nodeType===3)
	    if (text) return text+arg.nodeValue; else return arg.nodeValue;
	else if (arg.nodeType===1) {
	    var children=arg.childNodes;
	    var style=((window.getComputedStyle)?
		       (window.getComputedStyle(arg)):
		       {position: 'static',display: 'block'});
	    if (style.position!=='static') return text||"";
	    if (style.display!=='inline')
		text="\n"+(text||"");
	    else if (!(text)) text="";
	    var i=0; var lim=children.length;
	    while (i<lim) {
		var child=children[i++];
		if (child.nodeType===3) text=text+child.nodeValue;
		else if (child.nodeType===1) text=textify(child,text);
		else {}}
	    return text;}
	else if (text) return text;
	else return "";}
    WSN.prototype.textify=WSN.textify=textify;

    function fuddle(arg,sortfn){return WSN(arg,sortfn||lensort);}
    WSN.fuddle=fuddle;

    function md5ID(arg){
	var wsn=WSN.apply(null,arguments);
	if (WSN.md5) return WSN.md5(wsn);
	else if ((fdjtHash)&&(fdjtHash.hex_md5))
	    return fdjtHash.hex_md5(wsn);
	else throw new Exception("No MD5 implementation");}
    WSN.md5ID=md5ID;
    
    function sha1ID(arg){
	var wsn=WSN.apply(null,arguments);
	if (WSN.sha1) return WSN.md5(wsn);
	else if ((fdjtHash)&&(fdjtHash.hex_sha1))
	    return fdjtHash.hex_sha1(wsn);
	else throw new Exception("No MD5 implementation");}
    WSN.sha1ID=sha1ID;

    function Hash(arg,hashfn,sortfn,wordfn,keepdups){
	if (typeof hashfn === 'undefined') hashfn=WSN.hashfn||false;
	if (typeof sortfn === 'undefined') sortfn=WSN.sortfn||false;
	if (typeof wordfn === 'undefined') wordfn=WSN.wordfn||false;
	if (typeof keepdup === 'undefined') keepdup=WSN.keepdup||false;
	var wsn=WSN(arg,sortfn,wordfn,keepdups);
	return ((hashfn)?(hashfn(wsn)):(wsn));}
    WSN.Hash=Hash;
    WSN.prototype.Hash=function(arg){
	return Hash(arg,this.hashfn||WSN.hashfn||false,
		    this.sortfn||WSN.sortfn||false,
		    this.wordfn||WSN.wordfn||false,
		    this.keepdup||WSN.keepdup||false);}

    function Map(nodes,hashfn,sortfn,wordfn,keepdups){
	if (typeof hashfn === 'undefined') hashfn=WSN.hashfn||false;
	if (typeof sortfn === 'undefined') sortfn=WSN.sortfn||false;
	if (typeof wordfn === 'undefined') wordfn=WSN.wordfn||false;
	if (typeof keepdup === 'undefined') keepdup=WSN.keepdup||false;
	var map={};
	var i=0; var lim=nodes.length;
	while (i<lim) {
	    var node=nodes[i++];
	    var wsn=WSN(node,sortfn,wordfn,keepdups);
	    var id=((hashfn)?(hashfn(wsn)):(wsn));
	    map[id]=node;}
	return map;}
    WSN.Map=Map;
    WSN.prototype.Map=function(arg){
	return Map(arg,this.hashfn||WSN.hashfn||false,
		   this.sortfn||WSN.sortfn||false,
		   this.wordfn||WSN.wordfn||false,
		   this.keepdup||WSN.keepdup||false);}
    
    function MapMD5(nodes,sortfn,wordfn,keepdups){
	var hashfn=WSN.md5||((fdjtHash)&&(fdjtHash.hex_md5));
	return Map(nodes,hashfn,sortfn,wordfn,keepdups);}
    function MapSHA1(nodes,sortfn,wordfn,keepdups){
	var hashfn=WSN.sha1||((fdjtHash)&&(fdjtHash.hex_sha1));
	return Map(nodes,hashfn,sortfn,wordfn,keepdups);}

    WSN.md5=((fdjtHash)&&(fdjtHash.hex_md5));
    WSN.sha1=((fdjtHash)&&(fdjtHash.hex_sha1));

    try {
	if (("A\u0300".search(unicode_regex))<0)
	    unicode_regex=false;}
    catch (ex) {
	unicode_regexes=false;}
    
    return WSN;})();

/* -*- Mode: Javascript; Character-encoding: utf-8; -*- */

/* Copyright (C) 2009-2011 beingmeta, inc.
   This file provides a Javascript/ECMAScript of KNODULES, 
   a lightweight knowledge representation facility.

   For more information on knodules, visit www.knodules.net
   For more information about beingmeta, visit www.beingmeta.com

   This library is built on the FDJT (www.fdjt.org) toolkit.

   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 the GNU General Public License (GPL) Version 2:

   http://www.gnu.org/licenses/old-licenses/gpl-2.0.html

   Use and redistribution (especially embedding in other
   CC licensed content) is permitted under the terms of the
   Creative Commons "Attribution-NonCommercial" license:

   http://creativecommons.org/licenses/by-nc/3.0/ 

   Other uses may be allowed based on prior agreement with
   beingmeta, inc.  Inquiries can be addressed to:

   licensing@biz.beingmeta.com

   Enjoy!

*/

var Knodule=
    (function(){
	var knodules={};
	var all_knodules=[];
	var knodule=false;
	var trace_parsing=0;
	var trace_edits=false;

	var kno_simple_oidpat=/@[0-9A-Fa-f]+\/[0-9A-Fa-f]+/;
	var kno_named_oidpat=/@[0-9A-Fa-f]+\/[0-9A-Fa-f]+(\x22([^\x22]+)\x22)*/;
	var kno_atbreak=/[^\\]@/g;
	
	function Knodule(id,lang) {
	    // Raw cons
	    if (!(id)) return this;
	    // Do lookup
	    if (knodules[id])
		if ((lang)&&(lang!==knodules[id].language))
		    throw { error: "language mismatch" };
	    else return knodules[id];
	    if (fdjtKB.Pool.probe(id))
		throw { error: "pool/knodule conflict"};
	    if (this instanceof Knodule)
		knodule=fdjtKB.Pool.call(this,id);
	    else knodule=fdjtKB.Pool.call((new Knodule()),id);
	    // The name of the knodule
	    knodule.name=id;
	    knodules[id]=knodule;
	    // The default language for this knodule
	    if (lang) knodule.language=lang.toUpperCase();
	    else knodule.language='EN';
	    // Mapping strings to KNode objects (many-to-one)
	    knodule.dterms={};
	    // A vector of all dterms local to this knodule
	    knodule.alldterms=[];
	    // Prime dterms
	    knodule.prime=[]; knodule.primescores={};
	    // Whether the knodule is indexed (e.g. keeps inverse indices for
	    // relations and rules)
	    knodule.index=fdjtKB.Index();
	    // Whether to validate asserted relations
	    knodule.validate=true;
	    // Whether the knodule is 'strict'
	    // (requiring dterm definitions for all references)
	    knodule.strict=false;
	    // Whether the knodule is 'finished' (all references declared)
	    knodule.finished=false;
	    // Terms which are assumed unique.  This is used in non-strict
	    // knodules to catch terms that become ambiguous.
	    knodule.assumed_dterms=[];
	    // Mapping external dterms to knowdes
	    knodule.xdterms={};
	    // A vector of all foreign references
	    knodule.allxdterms=[];
	    // Mapping terms to arrays of of KNodes (ambiguous)
	    knodule.terms={};
	    // Mapping hook terms to arrays of of KNodes (ambiguous)
	    knodule.hooks={};
	    // Inverted indices
	    knodule.genlsIndex={};
	    // This maps external OIDs to knowdes
	    knodule.oidmap={};
	    // DRULES (disambiguation rules)
	    knodule.drules={};
	    return knodule;}
	Knodule.prototype=new fdjtKB.Pool();

	function KNode(knodule,string,lang){
	    if (!(knodule)) return this;
	    var weak=false; var prime=
		((string[0]==='*')&&(string.search(/[^*]/)));
	    var newprime=false;
	    if (string[0]==='~') {weak=true; string=string.slice(1);}
	    else if (prime) {
		string=string.slice(prime);
		if (!(knodule.primescores[string])) {
		    if (prime>(knodule.primescores[string]))
			knodule.primescores[string]=prime;
		    newprime=true;}}
	    if (string.search(/[a-zA-Z]\$/)===0) {
		lang=string.slice(0,2).toUpperCase();
		string=string.slice(3);}
	    else if (lang) lang=lang.toUpperCase();
	    else lang=knodule.language;
	    var term=string;
	    if (knodule.language!==lang) term=lang+"$"+string;
	    if (knodule.dterms.hasOwnProperty(term))
		return knodule.dterms[term];
	    /* Try taking this out, knodule name is implicit
	       in structure */
	    /*
	    var dterm=((this instanceof KNode)?
		       (knodule.ref(string+"@"+knodule.name,this)):
		       (knodule.ref(string+"@"+knodule.name)));
	    */
	    var dterm=((this instanceof KNode)?
		       (knodule.ref(string,this)):
		       (knodule.ref(string)));
	    dterm.dterm=term;
	    if (weak) dterm.weak=true;
	    if (prime) dterm.prime=prime;
	    if ((prime)&&(newprime)) knodule.prime.push(dterm);
	    knodule.dterms[dterm._id]=dterm;
	    knodule.dterms[term]=dterm;
	    knodule.alldterms.push(dterm);
	    if ((lang)&&(lang!==knodule.language)) dterm.language=lang;
	    dterm._always=fdjtKB.Set();
	    dterm.knodule=knodule;
	    dterm.addTerm(string,lang);
	    return dterm;}
	KNode.prototype=new fdjtKB.Ref();

	Knodule.KNode=KNode;
	Knodule.prototype.KNode=function(string,lang) {
	    if (string instanceof KNode) {
		if (string.pool===this)
		    // Should this do some kind of import?
		    return string;
		else return string;}
	    else return new KNode(this,string,lang);};
	Knodule.prototype.cons=function(string,lang) {
	    return new KNode(this,string,lang);};
	Knodule.prototype.probe=function(string,langid) {
	    if ((this.language===langid)||(!(langid)))
		return this.dterms[string]||false;
	    else return this.dterms[langid+"$"+string]||false;};
	
	KNode.prototype.add=function(prop,val){
	    if ((fdjtKB.Ref.prototype.add.call(this,prop,val))&&
		(prop==='genls')) {
		fdjtKB.add(this._always,val);
		fdjtKB.merge(this._always,val._always);
		return true;}
	    else return false;}
	KNode.prototype.addTerm=function(val,field){
	    if (val.search(/[a-zA-Z]\$/)===0)
		if (field)
		    this.add(val.slice(0,2)+'$'+field,val.slice(3));
	    else this.add(val.slice(0,2),val.slice(3));
	    else if (field) this.add(field,val);
	    else this.add(this.knodule.language,val);};
	KNode.prototype.tagString=function(kno) {
	    if ((kno===this.knodule)||(!(kno))) return this.dterm;
	    else return this.dterm+"@"+this.knodule.name;};
	KNode.prototype.newtagString=function(kno) {
	    return this.dterm;}; /* return this.dterm+"@"+this.knodule.name; */
	/* Text processing utilities */
	function stdspace(string) {
	    return string.replace(/\s+/," ").
		replace(/^\s/,"").replace(/\s$/,"");}
	
	function trimspace(string) {
	    return string.replace(/^\s/,"").replace(/\s$/,"");}

	function findBreak(string,brk,start) {
	    var pos=string.indexOf(brk,start||0);
	    while (pos>0)
		if (string[pos-1]!="\\")
		    return pos;
	    else pos=string.indexOf(brk,pos+1);
	    return pos;}

	var _knodule_oddpat=/(\\)|(\s\s)|(\s;)|(\s;)/g;
	
	function segmentString(string,brk,start,keepspace) {
	    if (start)
		if (string.slice(start).search(_knodule_oddpat)<0)
		    return string.slice(start).split(brk);
	    else {}
	    else if (string.search(_knodule_oddpat)<0)
		return string.split(brk);
	    else {}
	    var result=[]; var i=0, pos=start||0;
	    var nextpos=findBreak(string,brk,pos);
	    while (nextpos>=0) {
		var item=((keepspace) ? (string.slice(pos,nextpos)) :
			  (stdspace(string.slice(pos,nextpos))));
		if ((item) && (item !== "")) result.push(item);
		pos=nextpos+1;
		nextpos=findBreak(string,brk,pos);}
	    result.push(string.slice(pos));
	    return result;}
	function stripComments(string) {
	    return string.replace(/^\s*#.*$/g,"").
		replace(/^\s*\/\/.*$/g,"");}

	/* Processing the PLAINTEXT microformat */
	function handleClause(clause,subject) {
	    if (clause.indexOf('\\')>=0) clause=fdjtString.unEscape(clause);
	    if (trace_parsing>2)
		fdjtLog("Handling clause '%s' for %o",clause,subject);
	    switch (clause[0]) {
	    case '^':
		if (clause[1]==='~') 
		    subject.add('sometimes',this.KNode(clause.slice(2)));
		else if (clause[2]==='*') 
		    subject.add('commonly',this.KNode(clause.slice(2)));
		else {
		    var pstart=findBreak(clause,"(");
		    if (pstart>0) {
			var pend=findBreak(clause,")",pstart);
			if (pend<0) {
			    fdjtLog.warn(
				"Invalid Knodule clause '%s' for %o (%s)",
				clause,subject,subject.dterm);}
			else {
			    var role=this.KNode(clause.slice(1,pstart));
			    var object=this.KNode(clause.slice(pstart+1,pend));
			    object.add(role.dterm,subject);
			    subject.add('genls',role);}}
		    else subject.add('genls',this.KNode(clause.slice(1)));}
		break;
	    case '_': {
		var object=this.KNode(clause.slice(1));
		subject.add('examples',object);
		object.add('genls',subject);
		break;}
	    case '-':
		subject.add('never',this.KNode(clause.slice(1)));
		break;
	    case '&': {
		var value=clause.slice((clause[1]==="-") ? (2) : (1));
		var assoc=this.KNode(value);
		if (clause[1]==="-")
		    subject.add('antiassocs',assoc);
		else subject.add('assocs',assoc);
		break;}
	    case '@': 
		if (clause[1]==="#") 
		    subject.add('tags',clause.slice(2));
		else subject.add('uri',clause.slice(1));
		break;
	    case '=':
		if (clause[1]==='@')
		    subject.oid=clause.slice(1);
		else if (clause[1]==='*')
		    subject.add('equiv',this.KNode(clause.slice(2)));
		else if (clause[1]==='~')
		    subject.add('kinda',this.KNode(clause.slice(2)));
		else 
		    subject.add('identical',this.KNode(clause.slice(1)));
		break;
	    case '"': {
		var qend=((clause[-1]==='"') ? (-1) : (false));
		var gloss=((qend)?(clause.slice(2,qend)):(clause.slice(2)));
		if (clause[1]==="*") {
		    subject.gloss=gloss.slice(1);
		    subject.addTerm(subject.gloss,'glosses');}
		else {
		    subject.addTerm(gloss,"glosses");}
		break;}
	    case '%': {
		var mirror=this.KNode(clause.slice(1));
		if (subject.mirror===mirror) break;
		else {
		    var omirror=subject.mirror;
		    fdjtLog.warn("Inconsistent mirrors for %s: +%s and -%s",
				 subject,mirror,omirror);
		    omirror.mirror=false;}
		if (mirror.mirror) {
		    var oinvmirror=mirror.mirror;
		    fdjtLog.warn("Inconsistent mirrors for %s: +%s and -%s",
				 mirror,subject,oinvmirror);
		    omirror.mirror=false;}
		subject.mirror=mirror; mirror.mirror=subject;
		break;}
	    case '.': {
		var brk=findBreak(clause,'=');
		if (!(brk))
		    throw {name: 'InvalidClause', irritant: clause};
		var role=this.KNode(clause.slice(1,brk));
		var object=this.KNode(clause.slice(brk+1));
		this.add(role.dterm,object);
		object.add('genls',role);
		break;}
	    default: {
		var brk=findBreak(clause,'=');
		if (brk>0) {
		    var role=this.KNode(clause.slice(0,brk));
		    var object=this.KNode(clause.slice(brk+1));
		    subject.add(role.dterm,object);
		    object.add('genls',role);}
		else subject.addTerm(clause);}}
	    return subject;}
	Knodule.prototype.handleClause=handleClause;

	function handleSubjectEntry(entry){
	    var clauses=segmentString(entry,"|");
	    var subject=this.KNode(clauses[0]);
	    if (this.trace_parsing>2)
		fdjtLog("Processing subject entry %s %o %o",
			entry,subject,clauses);
	    var i=1; while (i<clauses.length)
		this.handleClause(clauses[i++],subject);
	    if (this.trace_parsing>2)
		fdjtLog("Processed subject entry %o",subject);
	    return subject;}
	Knodule.prototype.handleSubjectEntry=handleSubjectEntry;

	function handleEntry(entry){
	    entry=trimspace(entry);
	    if (entry.length===0) return false;
	    var bar=fdjtString.findSplit(entry,'|');
	    var atsign=fdjtString.findSplit(entry,'@');
	    if ((atsign>0) && ((bar<0)||(atsign<bar))) {
		var term=entry.slice(0,atsign);
		var knostring=((bar<0) ? (entry.slice(atsign+1)) :
			       (entry.slice(atsign+1,bar)));
		var knodule=Knodule(knostring);
		if (bar<0) return knodule.KNode(term);
		else return knodule.handleEntry(term+entry.slice(bar));}
	    switch (entry[0]) {
	    case '*': {
		var score=entry.search(/[^*]/);
		var trimmed=entry.slice(score);
		var subject=this.handleSubjectEntry(trimmed);
		var prime=this.prime; var scores=this.primescores;
		if (scores[subject._id])
		    scores[subject._id]=scores[subject._id]+score;
		else {
		    prime.push(subject);
		    scores[subject._id]=score;}
		return subject;}
	    case '-': {
		var subentries=segmentString(entry.slice(1),"/");
		var knowdes=[];
		var i=0; while (i<subentries.length) {
		    knowdes[i]=this.KNode(subentries[i]); i++;}
		var j=0; while (j<knowdes.length) {
		    var k=0; while (k<knowdes.length) {
			if (j!=k) knowdes[j].add('disjoin',knowdes[k]);
			k++;}
		    j++;}
		return knowdes[0];}
	    case '/': {
		var pos=1; var subject=false; var head=false;
		var next=findBreak(entry,'/',pos);
		while (true) {
		    var basic_level=false;
		    if (entry[pos]==='*') {basic_level=true; pos++;}
		    var next_subject=
			((next) ? (this.handleSubjectEntry(entry.slice(pos,next))) :
			 (this.handleSubjectEntry(entry.slice(pos))));
		    if (subject) subject.add('genls',next_subject);
		    else head=next_subject;
		    subject=next_subject;
		    if (basic_level) subject.basic=true;
		    if (next) {
			pos=next+1; next=findBreak(entry,"/",pos);}
		    else break;}
		return head;}
	    default:
		return this.handleSubjectEntry(entry);}}
	Knodule.prototype.handleEntry=handleEntry;

	function handleEntries(block){
	    if (typeof block === "string") {
		var nocomment=stripComments(block);
		var segmented=segmentString(nocomment,';');
		if (this.trace_parsing>1)
		    fdjtLog("Handling %d entries",segmented.length);
		return this.handleEntries(segmented);}
	    else if (block instanceof Array) {
		var results=[];
		var i=0; while (i<block.length) {
		    results[i]=this.handleEntry(block[i]); i++;}
		return results;}
	    else throw {name: 'TypeError', irritant: block};}
	Knodule.prototype.handleEntries=handleEntries;

	Knodule.prototype.def=handleSubjectEntry;

	Knodule.def=function(string,kno){
	    if (!(kno)) kno=Knodule.knodule;
	    return kno.def(string);};

	Knodule.prototype.trace_parsing=0;

	return Knodule;})();

var KNode=Knodule.KNode;


var KnoduleIndex=(function(){
    var isobject=fdjtKB.isobject;
    var objectkey=fdjtKB.objectkey;
    
    function KnoduleIndex(knodule) {
	if (knodule) this.knodule=knodule;
	this.byweight={}; this.bykey={}; this.tagweights={}; this._alltags=[];
	return this;}
    
    KnoduleIndex.prototype.add=function(item,key,weight,kno){
	if (key instanceof KNode) {
	    key=((key.tagString)?(key.tagString()):(key.dterm));}
	if (typeof weight !== 'number')
	    if (weight) weight=2; else weight=0;
	if ((weight)&&(!(this.byweight[weight])))
	    this.byweight[weight]={};
	var itemkey=((typeof item === 'object')?(objectkey(item)):(item));
	if (this.bykey.hasOwnProperty(key))
	    fdjtKB.add(this.bykey[key],itemkey);
	else {
	    this.bykey[key]=fdjtKB.Set(itemkey);
	    this._alltags.push(key);}
	if (weight) {
	    var byweight=this.byweight[weight];
	    if (byweight.hasOwnProperty(key))
		fdjtKB.add(byweight[key],itemkey);
	    else byweight[key]=fdjtKB.Set(itemkey);
	    (this.tagweights[key])=((this.tagweights[key])||0)+weight;}
	if (kno) {
	    var dterm=kno.probe(key);
	    if ((dterm)&&(dterm._always)) {
		var always=dterm._always;
		var i=0; var len=always.length;
		while (i<len)
		    this.add(itemkey,always[i++].dterm,((weight)&&(weight-1)));}}};
    KnoduleIndex.prototype.freq=function(key){
	if (this.bykey.hasOwnProperty(key))
	    return this.bykey[key].length;
	else return 0;};
    KnoduleIndex.prototype.find=function(key){
	if (this.bykey.hasOwnProperty(key)) return this.bykey[key];
	else return [];};
    KnoduleIndex.prototype.score=function(key,scores){
	var byweight=this.byweight;
	if (!(scores)) scores={};
	for (weight in byweight)
	    if (byweight[weight][key]) {
		var hits=byweight[weight][key];
		var i=0; var len=hits.length;
		while (i<len) {
		    var item=hits[i++]; var cur;
		    if (cur=scores[item]) scores[item]=cur+weight;
		    else scores[item]=weight;}}
	return scores;};
    KnoduleIndex.prototype.tagScores=function(){
	if (this._tagscores) return this._tagscores;
	var tagscores={}; var tagfreqs={}; var alltags=[];
	var book_tags=this._all; var max_score=0; var max_freq=0;
	var byweight=this.byweight;
	for (var w in byweight) {
	    var tagtable=byweight[w];
	    for (var tag in tagtable) {
		var howmany=tagtable[tag].length; var score; var freq;
		if (tagscores[tag]) {
		    score=tagscores[tag]=tagscores[tag]+w*howmany;
		    freq=tagfreqs[tag]=tagfreqs[tag]+howmany;}
		else {
		    score=tagscores[tag]=w*howmany;
		    freq=tagfreqs[tag]=howmany;	      
		    alltags.push(tag);}
		if (score>max_score) max_score=score;
		if (freq>max_freq) freq=max_freq;}}
	tagfreqs._count=alltags.length;
	alltags.sort(function (x,y) {
	    var xlen=tagfreqs[x]; var ylen=tagfreqs[y];
	    if (xlen==ylen) return 0;
	    else if (xlen>ylen) return -1;
	    else return 1;});
	tagscores._all=alltags; tagscores._freq=tagfreqs;
	tagscores._maxscore=max_score; tagscores._maxfreq=max_freq;
	this._tagscores=tagscores;
	return tagscores;};

    // This takes an array of tags (with possible .scores)
    //  and combines them into an array with unique elements
    //  and combined scores
    function combineTags(tagsets,weights){
	var tags=[]; var scores={};
	var tagscores=[];
	var i=0; var lim=tagsets.length;
	while (i<lim) {
	    if ((tagsets[i])&&(tagsets[i].scores))
		tagscores.push(tagsets[i].scores);
	    i++;}
	var i=0; var lim=tagsets.length;
	while (i<lim) {
	    var taglist=tagsets[i++];
	    if (!(taglist)) continue;
	    var j=0; var jlim=taglist.length;
	    while (j<jlim) {
		var tag=taglist[j]; var score=0;
		if (!(scores[tag])) {
		    tags.push(tag);
		    var k=0; var klim=tagscores.length;
		    while (k<klim) {
			var tscore=tagscores[k++][tag];
			if (tscore) score=score+tscore;}}
		scores[tag]=(((weights)&&(weights[i]))||1)+score;
		j++;}}
	tags.scores=tagscores;
	return tags;}
    KnoduleIndex.combineTags=combineTags;
    KnoduleIndex.prototype.combineTags=combineTags;

    return KnoduleIndex;})();


/* Emacs local variables
   ;;;  Local variables: ***
   ;;;  compile-command: "cd ..; make" ***
   ;;;  End: ***
*/
/* -*- Mode: Javascript; Character-encoding: utf-8; -*- */

/* Copyright (C) 2009-2011 beingmeta, inc.
   This file provides a Javascript/ECMAScript of KNODULES, 
     a lightweight knowledge representation facility.

   For more information on knodules, visit www.knodules.net
   For more information about beingmeta, visit www.beingmeta.com

   This library is built on the FDJT (www.fdjt.org) toolkit.

   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 the GNU General Public License (GPL) Version 2:

          http://www.gnu.org/licenses/old-licenses/gpl-2.0.html

    Use and redistribution (especially embedding in other
      CC licensed content) is permitted under the terms of the
      Creative Commons "Attribution-NonCommercial" license:

          http://creativecommons.org/licenses/by-nc/3.0/ 

    Other uses may be allowed based on prior agreement with
      beingmeta, inc.  Inquiries can be addressed to:

       licensing@biz.beingmeta.com

   Enjoy!

*/

KnoduleIndex.Query=
    (function(){
	function Query(index,query) {
	    if (!(index)) return this;
	    if (!(this instanceof Query)) return new Query(this,index);
	    if (typeof query === "string") query=this.string2query(query);
	    var qstring=this.query2string(query);
	    var cached=((index.cache)&&(index.cache[qstring]));
	    if (cached) return cached;
	    // Construct the results object
	    this.index=index; this._query=query; this._qstring=qstring;
	    this._results=[]; this._scores={};
	    if (query.length===0) {
		this._refiners={_results: index._alltags};
		return this;}
	    this._start=new Date();
	    // Do the search
	    this.do_search();
	    this._done=new Date();
	    if (this._refiners) {}
	    else this._refiners=this.get_refiners();
	    this._refined=new Date();
	    if (this.index.trace)
		fdjtLog("In %f secs, %o yielded %d results: %o",
			((this._done.getTime()-this._start.getTime())/1000),
			query,result._results.length,result._results);
	    if (this.index.trace)
		fdjtLog("In %f secs, query %o yielded %d refiners: %o",
			((this._refined.getTime()-this._done.getTime())/1000),
			query,result._refiners._results.length,
			result._refiners._results);
	    if (index.cache) index.cache[qstring]=this;
	    return this;}
	Knodule.Query=Query;
	KnoduleIndex.Query=Query;
	KnoduleIndex.prototype.Query=function(query){
	    return new Query(this,query);}

	// Queries are sets of terms and interchangable between vectors
	// and strings with semi-separated tag names

	function string2query(string) {
	    if (typeof string === "string") {
		var lastsemi=string.lastIndexOf(';');
		if (lastsemi>0)
		    return string.slice(0,lastsemi).split(';');
		else return [];}
	    else return string;}
	Query.string2query=string2query;
	Query.prototype.string2query=string2query;

	function query2string(query){
	    if (!(query)) query=this.query;
	    if ((typeof query === "object") && (query instanceof Array))
		if (query.length===0) return "";
	    else return query.join(';')+';';
	    else return query;}
	Query.prototype.cache={};
	Query.prototype.query2string=query2string;
	Query.prototype.getString=query2string;
	Query.query2string=query2string;

	function do_search(results) {
	    if (!(results)) results=this;
	    var query=results._query; var scores=results._scores;
	    var matches=[];
	    // A query is an array of terms.  In a simple query,
	    // the results are simply all elements which are tagged
	    // with all of the query terms.  In a linear scored query,
	    // a score is based on how many of the query terms are matched,
	    // possibly with weights based on the basis of the match.
	    var i=0; while (i<query.length) {
		var term=query[i];
		if (typeof term !== 'string') term=term._id||term.dterm;
		var items=matches[i]=results.index.find(term);
		if (results.index.trace)
		    fdjtLog("Query element '%s' matches %d items",
			    term,items.length);
		i++;}
	    var allitems=false;
	    if (query.length===1) allitems=matches[0];
	    else {
		var i=0; var lim=query.length;
		while (i<lim) {
		    var j=0; while (j<lim) {
			if (j>=i) {j++; continue}
			else if (matches[j].length===0) {j++; continue;}
			else if (allitems) {
			    var join=fdjtKB.intersection(matches[i],matches[j]);
			    allitems=fdjtKB.union(allitems,join);}
			else allitems=fdjtKB.intersection(matches[i],matches[j]);
			j++;}
		    i++;}}
	    results._results=allitems;
	    i=0; var n_items=allitems.length;
	    while (i<n_items) {
		var item=allitems[i++];
		var tags=results.index.Tags(item);
		var j=0; var lim=query.length; var cur;
		while (j<lim) {
		    var tag=query[j++];
		    if (cur=scores[item])
			scores[item]=cur+tags[item]||1;
		    else scores[item]=tags[item]||1;}}
	    // Initialize scores for all of results
	    return results;}
	Query.do_search=do_search;
	Query.prototype.do_search=function() { return do_search(this);};

	function get_refiners(results) {
	    if (!(results)) results=this;
	    // This gets terms which can refine this search, particularly
	    // terms which occur in most of the results.
	    if (results._refiners) return results._refiners;
	    var query=results._query;
	    var qterms=[];
	    var rvec=(results._results);
	    var refiners={};
	    var scores=(results._scores)||false; var freqs={};
	    var alltags=[];
	    var i=0; while (i<query.length) {
		var q=query[i++];
		qterms.push(q);
		if (q.dterm) qterms.push(q.dterm);
		if (q.dterms) {
		    var dterms=q.dterms; var j=0;
		    while (j<dterms.length) qterms.push(dterms[j++]);}}
	    var i=0; while (i<rvec.length) {
		var item=rvec[i++];
		var item_score=((scores)&&(scores[item]));
		var tags=results.index.Tags(item)||[];
		if (typeof tags === 'string') tags=[tags];
		if (tags) {
		    var j=0; var len=tags.length; while (j<len) {
			var tag=tags[j++];
			// If the tag is already part of the query, we ignore it.
			if (fdjtKB.contains(qterms,tag)) {}
			// If the tag has already been seen, we increase its frequency
			// and its general score
			else if (freqs[tag]) {
			    freqs[tag]=freqs[tag]+1;
			    if (item_score) refiners[tag]=refiners[tag]+item_score;}
			else {
			    // If the tag hasn't been counted, we initialize its frequency
			    // and score, adding it to the list of all the tags we've found
			    alltags.push(tag); freqs[tag]=1;
			    if (item_score) refiners[tag]=item_score;}}}}
	    freqs._count=rvec.length;
	    refiners._freqs=freqs;
	    results._refiners=refiners;
	    alltags.sort(function(x,y) {
		if (freqs[x]>freqs[y]) return -1;
		else if (freqs[x]===freqs[y]) return 0;
		else return 1;});
	    refiners._results=alltags;
	    if ((results.index.trace)&&(results.index.trace>1))
		fdjtLog("Refiners for %o are (%o) %o",
			results._query,refiners,alltags);
	    return refiners;}
	Query.get_refiners=get_refiners;
	Query.prototype.get_refiners=function() {return get_refiners(this);};

	/* Dead code? */
	/*
	  Query.base=function(string) {
	  var lastsemi=string.lastIndexOf(';');
	  if (lastsemi>0)
	  return string.slice(0,lastsemi+1);
	  else return "";};
	  Query.tail=function(string) {
	  var lastsemi=string.lastIndexOf(';');
	  if (lastsemi>0)
	  return string.slice(lastsemi+1);
	  else return string;};

	*/
	return Query;
    })();


/* Emacs local variables
;;;  Local variables: ***
;;;  compile-command: "cd ..; make" ***
;;;  End: ***
*/

/* -*- Mode: Javascript; Character-encoding: utf-8; -*- */

/* Copyright (C) 2009-2011 beingmeta, inc.
   This file provides for HTML documents using KNODULES, including
   the extraction and processing of embedded KNODULE definitions
   or references and interaction with interactive parts of the
   FDJT library.

   For more information on knodules, visit www.knodules.net
   This library is built on the FDJT (www.fdjt.org) toolkit.

   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 the GNU General Public License (GPL) Version 2:

   http://www.gnu.org/licenses/old-licenses/gpl-2.0.html

   Use and redistribution (especially embedding in other
   CC licensed content) is permitted under the terms of the
   Creative Commons "Attribution-NonCommercial" license:

   http://creativecommons.org/licenses/by-nc/3.0/ 

   Other uses may be allowed based on prior agreement with
   beingmeta, inc.  Inquiries can be addressed to:

   licensing@biz.beingmeta.com

   Enjoy!

*/

(function(){

    var knodules_trace_load=2;

    /* Getting knowdes into HTML */

    var KNode=Knodule.KNode;
    Knodule.KNode.prototype.toHTML=function(lang){
	var spec=((this.prime)?("span.dterm.prime"):
		  (this.weak)?("span.dterm.weak"):
		  "span.dterm");
	if (this.gloss) {
	    var span=fdjtDOM(spec,this.dterm);
	    span.title=fdjtString.strip_markup(this.gloss);
	    span.dterm=this.dterm;
	    return span;}
	else return fdjtDOM(spec,this.dterm);};

    /* Making DTERM descriptions */

    function knoduleHTML(dterm,kno,varname,lang){
	var checkbox=false; var variations=[];
	var text=dterm; var def=false;
	// A non-false language arg generates a completion, and a
	// non-string language arg just uses the knodules default language
	if ((lang)&&(typeof lang !== 'string')) {
	    if (kno) lang=kno.language; else lang='EN';}
	// Resolve the KNode if you can
	if ((kno)&&(typeof dterm === 'string'))
	    if (kno.probe(dterm)) {
		dterm=kno.probe(dterm);
		text=dterm.dterm;}
	else if (dterm.indexOf('|')>=0) {
	    var pos=dterm.indexOf('|');
	    def=dterm.slice(pos);
	    dterm=kno.handleSubjectEntry(dterm);
	    text=dterm.dterm;}
	else dterm=dterm;
	var tagstring=false;
	if ((varname)||(lang)) {
	    tagstring=((dterm.tagString)?(dterm.tagString()):
		       ((dterm._id)||(dterm)));
	    if (def) tagstring=tagstring+def;}
	if (varname) 
	    checkbox=fdjtDOM
	({tagName: "INPUT",type: "CHECKBOX",name: varname,value: tagstring});
	if ((lang)&&(dterm instanceof KNode)) {
	    var synonyms=dterm[lang];
	    if ((synonyms)&&(typeof synonyms === 'string'))
		synonyms=[synonyms];
	    if (synonyms) {
		var i=0; while (i<synonyms.length) {
		    var synonym=synonyms[i++];
		    if (synonym===dterm) continue;
		    var variation=fdjtDOM("span.variation",synonym,"=");
		    variation.setAttribute("key",synonym);
		    variations.push(variation);}}}
	var span=fdjtDOM("span.knode",checkbox,variations,text);
	if (varname) fdjtDOM.addClass(span,"checkspan");
	if (typeof text !== 'string') text=tagstring;
	if (lang) {
	    fdjtDOM.addClass(span,"completion");
	    span.key=text; span.value=tagstring;}
	if (!(dterm instanceof KNode)) fdjtDOM.addClass(span,"raw");
	if (dterm.gloss) span.title=dterm.gloss;
	return span;};
    Knodule.HTML=knoduleHTML;
    Knodule.prototype.HTML=function(dterm){
	var args=new Array(arguments.length+1);
	args[0]=arguments[0]; args[1]=this;
	var i=1; var lim=arguments.length; while (i<lim) {
	    args[i+1]=arguments[i]; i++;}
	return knoduleHTML.apply(this,args);};

    /* Getting Knodules out of HTML */

    var _knodulesHTML_done=false;

    function KnoduleLoad(elt,knodule){
	var src=((typeof elt === 'string')?(elt):(elt.src));
	var text=fdjtAjax.getText(src);
	var knowdes=knodule.handleEntries(text);
	if (knodules_trace_load)
	    fdjtLog("Parsed %d entries from %s",knowdes.length,elt.src);}

    function knoduleSetupHTML(knodule){
	if (!(knodule)) knodule=Knodule(document.location.href);
	var doing_the_whole_thing=false;
	var start=new Date();
	var links=fdjtDOM.getLink("knodule",true,false).
	    concat(fdjtDOM.getLink("knowlet",true,false));
	var i=0; while (i<links.length) KnoduleLoad(links[i++],knodule);
	var elts=document.getElementsByTagName("META");
	var i=0; while (i<elts.length) {
	    var elt=elts[i++];
	    if (elt.name==="KNOWDEF") knodule.handleEntry(elt.content);}
	elts=document.getElementsByTagName("SCRIPT");
	i=0; while (i<elts.length) {
	    var elt=elts[i++];
	    var lang=elt.getAttribute("language");
	    var type=elt.type;
	    if ((type==="text/knodule")||(type==="application/knodule")||
		((lang) &&
		 ((lang==="knodule") ||(lang==="KNODULE")||
		  (lang==="knowlet"||(lang==="KNOWLET"))))) {
		if (elt.src) KnoduleLoad(elt,knodule);
		else if (elt.text) {
		    var txt=elt.text;
		    var cdata=txt.search("<!\\[CDATA\\[");
		    if (cdata>=0) {
			var cdend=txt.search("]]>");
			txt=txt.slice(cdata+9,cdend);}
		    var dterms=knodule.handleEntries(txt);
		    if (knodules_trace_load)
			fdjtLog("Parsed %d inline knodule entries",
				dterms.length);}
		else {}}}
	var finished=new Date();
	if (knodules_trace_load)
	    fdjtLog("Processed knodules in %fs",
		    ((finished.getTime()-start.getTime())/1000));}
    Knodule.HTML.Setup=knoduleSetupHTML;

})();

/* Emacs local variables
   ;;;  Local variables: ***
   ;;;  compile-command: "cd ..; make" ***
   ;;;  End: ***
*/
/* -*- Mode: Javascript; Character-encoding: utf-8; -*- */

/* Copyright (C) 2009-2011 beingmeta, inc.
   This file is a part of the FDJT web toolkit (www.fdjt.org)
   It implements a method for breaking narrative HTML content
   across multiple pages, attempting to honor page break constraints,
   etc.

   Check out the 'mini manual' at the bottom of the file or read the
   code itself.

   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

   Use and redistribution (especially embedding in other CC licensed
   content) is also permitted under the terms of the Creative Commons
   "Attribution-NonCommercial" license:

   http://creativecommons.org/licenses/by-nc/3.0/ 

   Other uses may be allowed based on prior agreement with
   beingmeta, inc.  Inquiries can be addressed to:

   licensing@beingmeta.com

   Enjoy!

*/

var CodexLayout=
    (function(){

	var hasContent=fdjtDOM.hasContent;
	var getGeometry=fdjtDOM.getGeometry;
	var getDisplay=fdjtDOM.getDisplay;
	var hasParent=fdjtDOM.hasParent;
	var getParent=fdjtDOM.getParent;
	var getStyle=fdjtDOM.getStyle;
	var parsePX=fdjtDOM.parsePX;
	var geomString=fdjtDOM.geomString;
	var insertBefore=fdjtDOM.insertBefore;
	var hasClass=fdjtDOM.hasClass;
	var addClass=fdjtDOM.addClass;
	var dropClass=fdjtDOM.dropClass;
	var nextElt=fdjtDOM.nextElt;
	var forward=fdjtDOM.forward;
	var TOA=fdjtDOM.toArray;
	
	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 isEmpty(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;}
	
	function testNode(node,test) {
	    var tests;
	    if (!(test)) return true;
	    if (typeof test === 'string') tests=[test];
	    else if (test instanceof Array) tests=test;
	    else tests=[test];
	    var i=0; var lim=tests.length;
	    while (i<lim) {
		var atest=tests[i++];
		if (node===test) return true;
		else if (typeof atest === 'string') {
		    if (!(node.className)) continue;
		    var classrx=new Regexp("\b"+atest+"\b");
		    if (node.className.search(classrx)>=0) return true;}
		else if ((atest.match)&&(atest.match(node)))
		    // This should get most versions of CSS selectors
		    return true;
		else {}}
	    return false;}

	function insideBounds(box){
	    var top=false, right=false, bottom=false, left=false;
	    function gatherBounds(node){
		if ((!(node))||(node.nodeType!==1)) return;
		var style=getStyle(node); var children;
		if ((style.position==='static')&&
		    ((node.tagName==='img')||(style.display!=='inline'))) {
		    var geom=getGeometry(node,box);
		    if ((left===false)||(geom.left<left)) left=geom.left;
		    if ((top===false)||(geom.top<top)) top=geom.top;
		    if ((right===false)||(geom.right>right)) right=geom.right;
		    if ((bottom===false)||(geom.bottom>bottom))
			bottom=geom.bottom;}
		if ((children=node.childNodes)&&(children.length)) {
		    var i=0; var len=children.length;
		    while (i<len) gatherBounds(children[i++]);}}
	    var nodes=box.childNodes;
	    var j=0; var jlim=nodes.length;
	    while (j<jlim) gatherBounds(nodes[j++]);
	    return { left: left, top: top, right: right, bottom: bottom,
		     width: right-left, height: bottom-top};}
	
	var alerted=false;
	function default_logfn() {
	    if (alerted) return;
	    alert("Layout needs to log but cannot");
	    alerted=true;}
	
	/* Scaling things */
	
	function scaleImage(image,scale,geom){
	    if (!(geom)) geom=getGeometry(image);
	    // Set image width/height explicitly rather than
	    // scaling because that's more portable
	    var w=Math.round(geom.width*scale);
	    var h=Math.round(geom.height*scale);
	    image.width=image.style.width=
		image.style['max-width']=image.style['min-width']=w;
	    image.height=image.style.height=
		image.style['max-height']=image.style['min-height']=h;
	    addClass(image,"codextweaked");}
	
	/* Codex trace levels */
	/* 0=notrace (do final summary if tracing startup)
	   1=trace repagination chunk by chunk
	   2=trace inserted page breaks
	   3=trace every node consideration
	*/

	/* Duplicating nodes */

	var tmpid_count=1;

	// This recreates a node and it's DOM context (containers) on
	//  a new page, calling itself recursively as needed
	function dupContext(node,page,dups){
	    if ((node===document.body)||(node.id==="CODEXCONTENT")||
		(node.id==="CODEXROOT")||(hasClass(node,"codexroot"))||
		(hasClass(node,"codexpage")))
		return false;
	    else if (hasParent(node,page)) return node;
	    else if ((node.className)&&
		     (node.className.search(/\bcodexwraptext\b/)>=0))
		// We don't bother duplicating text wrapping convenience
		//  classes
		return dupContext(node.parentNode,page,dups);
	    // Now we actually duplicate it.  
	    var id=node.id;
	    // If it doesn't have an ID, we give it one, because we'll want
	    //  to refer to it later while wanting to avoid DOM cycles
	    if (!(id)) id=node.id="CODEXTMPID"+(tmpid_count++);
	    else {
		// See if it's already been duplicated
		var dup=dups[id];
		if ((dup)&&(hasParent(dup,page))) return dup;}
	    // Duplicate it's parent
	    var copy=node.cloneNode(false);
	    var parent=dupContext(node.parentNode,page,dups);
	    var nodeclass=node.className||"";
	    // Jigger the class name
	    copy.className=
		((nodeclass.replace(/\b(codexrelocated|codexdup.*)\b/,""))+
		 " codexdup").replace(/\s+/," ").trim();
	    if (nodeclass.search(/\bcodexdupstart\b/)<0)
		node.className=nodeclass+" codexdupstart";
	    // If the original had an ID, save it in various ways
	    if (node.id) {
		copy.codexdupid=node.id;
		copy.setAttribute("data-baseid",node.id);
		copy.id=null;}
	    // Record the copy you've made (to avoid recreation)
	    dups[id]=copy;
	    // If it's got a copied context, append it to the context;
	    //   otherwise, just append it to the page
	    if (parent) parent.appendChild(copy);
	    else page.appendChild(copy);
	    return copy;}

	/* Moving nodes */

	var codex_reloc_serial=1;
	
	// This moves a node into another container, leaving
	// a back pointer for restoration
	function moveNode(arg,into,blockp){
	    var baseclass; var node=arg;
	    // If we're moving a first child, we might as well move the parent
	    if (hasParent(node,into)) return node;
	    while ((node.parentNode)&&(node===node.parentNode.firstChild)&&
		   (node.parentNode!==document.body)&&
		   (node.parentNode!==Codex.content)&&
		   (!(hasClass(node.parentNode,"codexpage"))))
		node=node.parentNode;
	    if (node.nodeType===1) baseclass=node.className;
	    else if (node.nodeType===3) {
		// Wrap text nodes in elements before moving
		var wrapnode=fdjtDOM(
		    ((blockp)?"div.codexwraptext":"span.codexwraptext"));
		node.parentNode.replaceChild(wrapnode,node);
		wrapnode.appendChild(node);
		baseclass="codexwraptext";
		node=wrapnode;}
	    if ((node.parentNode)&&(!(node.getAttribute("data-codexorigin")))) {
		// Record origin information; we'll use this to revert
		//  the layout if we need to (for example, before
		//  laying out again under different constraints)
		var origin=fdjtDOM("span.codexorigin");
		var id=origin.id="CODEXORIGIN"+(codex_reloc_serial++);
		if (baseclass) node.className=baseclass+" codexrelocated";
		else node.className="codexrelocated";
		node.setAttribute("data-codexorigin",id);
		node.parentNode.replaceChild(origin,node);}
	    into.appendChild(node);
	    return node;}
	
	// This moves a node onto a page, recreating its original DOM
	// context on the new page.
	function moveNodeToPage(node,page,dups){
	    if ((!(page.getAttribute("data-topid")))&&
		(node.id)&&(Codex.docinfo[node.id])) {
		var info=Codex.docinfo[node.id];
		page.setAttribute("data-topid",node.id);
		page.setAttribute("data-sbookloc",info.starts_at);}
	    if (hasParent(node,page)) return node;
	    var parent=node.parentNode;
	    if ((!(parent))||(parent===document.body)||
		(parent.id==="CODEXCONTENT")||(parent.id==="CODEXROOT")||
		(hasClass(parent,"codexroot"))||(hasClass(parent,"codexpage")))
		// You don't need to dup the parent on the new page
		return moveNode(node,page);
	    else {
		var dup_parent=dupContext(parent,page,dups);
		return moveNode(node,dup_parent||page);}}

	// Reverting layout

	function restoreNode(node,info){
	    var originid=node.getAttribute("data-codexorigin");
	    var origin=((originid)&&document.getElementById(originid));
	    if (origin) {
		if (hasClass(node,/\bcodexwraptext\b/g)) {
		    if (hasClass(node,/\bcodexwraptextsplit\b/g))
			origin.parentNode.replaceChild(
			    info.texts[originid],origin);
		    else origin.parentNode.replaceChild(
			node.childNodes[0],origin);}
		else origin.parentNode.replaceChild(node,origin);}
	    dropClass(node,"codexrelocated");
	    node.removeAttribute("data-codexorigin");}
	
	function revertLayout(layout) {
	    var tweaked=TOA(layout.container.getElementsByClassName("codextweaked"));
	    if ((tweaked)&&(tweaked.length)) {
		layout.logfn("Dropping tweaks of %d relocated nodes",tweaked.length);
		var i=0; var lim=tweaked.length;
		while (i<lim) {
		    var node=tweaked[i++]; node.style='';
		    dropClass(node,"codextweaked");
		    if ((node.tagName==='img')||(node.tagName==='IMG')) {
			node.width=''; node.height='';}}}
	    var cantsplit=TOA(layout.container.getElementsByClassName("codextweaked"));
	    dropClass(cantsplit,"codexcantsplit");
	    var moved=TOA(layout.container.getElementsByClassName("codexrelocated"));
	    if ((moved)&&(moved.length)) {
		layout.logfn("Restoring original layout of %d relocated nodes and %d texts",
			     moved.length);
		var i=0; var lim=moved.length;
		while (i<lim) restoreNode(moved[i++],layout);}}
	
	function CodexLayout(init){
	    if (!(init)) init={};

	    var layout=this;

	    // Layout rules
	    var forcebreakbefore=this.forcebreakbefore=init.forcebreakbefore||false;
	    var forcebreakafter=this.forcebreakafter=init.forcebreakafter||false;
	    var avoidbreakinside=this.avoidbreakinside=init.avoidbreakinside||false;
	    var avoidbreakafter=this.avoidbreakafter=init.avoidbreakafter||false;
	    var avoidbreakbefore=this.avoidbreakbefore=init.avoidbreakbefore||false;
	    var pageblock=this.pageblock=init.pageblock||false;
	    var fullpages=this.fullpages=init.fullpages||false;
	    var floatpages=this.floatpages=init.floatpages||false;
	    var pageprefix=this.pageprefix=init.pageprefix||"CODEXPAGE";

	    // Layout Dimensions
	    var page_height=this.height=init.page_height||fdjtDOM.viewHeight();
	    var page_width=this.width=init.page_width||fdjtDOM.viewWidth();
	    
	    // This is the node DOM container where we place new pages
	    var container=this.container=init.container||fdjtDOM("div.codexpages");
	    
	    var logfn=this.logfn=
		init.logfn||CodexLayout.logfn||
		((typeof fdjtLog !== 'undefined')?(fdjtLog):(noop));

	    // STATE variables

	    var pagenum=this.pagenum=0; // Tracks current page number
	    var pages=this.pages=[]; // Array of all pages generated, in order
	    var dups=this.dups={}; // Tracks nodes/contexts already duplicated

	    // Tracks text nodes which have been split, keyed by the
	    // temporary IDs assigned to them
	    var textsplits=this.textsplits={};

	    var page=this.page=init.page; // Contains the currently open page

	    var prev=this.prev=false; // The last terminal block we processed
	    var prevstyle=this.prevstyle=false;

	    // this.drag[] contains nodes which will go on the next
	    // page when we get there.  The nodes in this.drag[] have
	    // already been placed on pages, but we keep track of them
	    // in case we need to move them to a new page to honor
	    // nobreak constraints.
	    var drag=this.drag=[];

	    // this.float_pages contains fully-assembled page nodes to
	    // placed in pages after this one; it is intended for
	    // out-of-flow or 'too bit to fit' content
	    var float_pages=this.float_pages=[];

	    // this.float_blocks is an array of blocks to place into
	    // the flow there is a chance.  This is intended for use
	    // by figures/tables/etc which we don't want to embed
	    var float_blocks=this.float_blocks=[];

	    // Startup

	    this.started=false; // When we started
	    var trace=this.tracelevel=  // How much to trace
		init.tracelevel||CodexLayout.tracelevel;
	    this.roots=init.roots||false; // Where all roots can be bracked
	    this.root_count=0; // Number of root nodes added
	    this.block_count=0;
	    
	    var pagerule=this.pagerule=init.pagerule||false;
	    
	    function addContent(root,trace) {

		if (!(page)) newPage();

		if (typeof trace === 'undefined') trace=layout.tracelevel;
		if (!(layout.started)) layout.started=fdjtTime();
		layout.root_count++;

		function loop(node){
		    var blocks=[], terminals=[], styles=[];
		    // gather all of the block-level elements
		    // (recursively) in the node, noting which ones
		    // are terminals
		    gatherBlocks(node,blocks,terminals,styles);
		    layout.block_count=layout.block_count+blocks.length;
		    // Then move the node onto the current page; we
		    // set node, because it might be transformed in
		    // some way when moved (if, for example, it is a
		    // text node, it will be wrapped).
		    node=moveNodeToPage(node,page,dups);
		    // Iterate over all of the blocks
		    var i=0, n=blocks.length; while (i<n) {
			var block=blocks[i]; var style=styles[i];
			var terminal=terminals[i]||false;
			// FIRST, HANDLE DRAGGING
			// If this block is terminal and we don't want
			//  to break before this block or after the
			//  preceding block, drag along the previous block.
			//  NOTE that dragged blocks have already been placed.
			if ((block)&&(terminal)&&(prev)&&
			    ((avoidBreakBefore(block,style))||
			     (avoidBreakAfter(prev,prevstyle))))
			    drag.push(prev);
			else if ((block)&&(terminal))
			    // Otherwise, we don't have to worry about
			    // what we're dragging along
			    layout.drag=drag=[];
			else {}
			// If a block is false, continue
			if (!(block)) {i++; continue;}
			else if ((hasClass(block,/\bcodexfloatpage\b/))||
				 ((floatpages)&&(testNode(block.floatpages)))) {
			    // Float pages just get pushed (until newPage bfelow)
			    float_pages.push[block]; i++; continue;}
			else if ((hasClass(block,/\bcodexfullpage\b/))||
				 ((fullpages)&&(testNode(block.fullpages)))) {
			    // Full pages automatically get their own page
			    prev=false; layout.drag=drag=[];
			    fullPage(block);
			    i++; continue;}
			else if ((page.childNodes.length)&&
				 (forcedBreakBefore(block,style))) {
			    // This is the easy case.  Note that we
			    // don't force a page break if the current
			    // page is empty.
			    prev=false; layout.drag=drag=[];
		    	    newPage(block);}
			else moveNodeToPage(block,page,dups);
			// Finally, we check if everything fits We're
			// walking through the blocks[] but only
			// advance when an element fits or can't be
			// split or tweaked Note that we may process
			// an element [i] more than once if we split
			// the node and part of the split landed back in [i].
			var geom=getGeometry(block,page);
			if (trace>2) logfn("Layout/loop %o %j",block,geom);
			if ((terminal)&&(geom.bottom>page_height)) {
			    // We're a terminal node and we extend
			    // below the bottom of the page
			    if (geom.top>page_height)
				// If our top is also over the bottom of the page,
				//  we just start a new page
				newPage(block);
			    else if (hasClass(block,"codexcantsplit")) {
				// If we can't split this block (this
				// class might be added by previous
				// processing), we try to tweak it
				// (which means using CSS magic for
				// scaling, etc).
				if (!((hasClass(block,"codextweaked"))||
				      (hasClass(block,"codexavoidtweak"))))
				    tweakBlock(block);
				i++;}
			    else {
				// Now we try to split the block, we
				// store the 'split block' back in the
				// blocks variable because we might
				// need to split it again.
				blocks[i]=splitBlock(block);
				// If the block couldn't be split, try to tweak it
				// Could this be removed entirely?
				if (hasClass(block,"codexcantsplit")) {
				    var geom=getGeometry(block,page);
				    if (geom.bottom>page_height) {
					// Still over the edge, so tweak it
					if (!(hasClass(block,"codexavoidtweak")))
					    tweakBlock(block);}
				    i++;}}}
			// We fit on the page, so we'll look at the next block.
			else i++;
			// Update the prev pointer for terminals
			if (terminal) {layout.prev=prev=block;}}}

		// Gather all the block-level elements inside a node,
		// recording which ones are terminals (don't have any
		// blocks within them)
		function gatherBlocks(node,blocks,terminals,styles){
		    if (node.nodeType!==1) return;
		    if (node.codexui) return;
		    var style=getStyle(node); var disp=style.display;
		    if ((style.position==='static')&&(disp!=='inline')) {
			var loc=blocks.length;
			blocks.push(node);
			styles.push(style);
			if (avoidBreakInside(node,style))
			    terminals[loc]=true;
			else if ((disp==='block')||(disp==='table')) {
			    var children=node.childNodes;
			    var total_blocks=blocks.length;
			    var i=0; var len=children.length;
			    while (i<len) {
				gatherBlocks(children[i++],
					     blocks,terminals,styles);}
			    if (blocks.length==total_blocks)
				terminals[loc]=true;}
			else terminals[loc]=true;}}

		// Whether we need to create a new page to have 'node'
		//  at the page top We don't need a new page if the
		//  current page has no content or no content up until
		//  the node in question
		function needNewPage(node){
		    if (!(page)) return true;
		    else if (!(node))
			return hasContent(page,true,true);
		    else if (!(hasParent(node,page)))
			return hasContent(page,true,true);
		    else if (page.firstChild===node)
			return false;
		    else if (hasContent(page,node,true))
			return true;
		    else return false;}

		// Cerate a new page
		function newPage(node){
		    // If node exists, it is the first element on the new page
		    if ((float_pages)&&(float_pages.length)) {
			// First add any floating pages that may have
			// accumulated
			var i=0; var lim=float_pages.length;
			while (i<lim) fullPage(float_pages[i++]);
			float_pages=[];}
		    var newpage="pagetop";
		    if (needNewPage(node)) {
			// If we really need to create a new page, do so
			if (page) dropClass(page,"curpage");
			layout.page=page=fdjtDOM("div.codexpage.curpage");
			if (!(pagerule)) {
			    page.style.height=page_height+'px';
			    page.style.width=page_width+'px';}
			pagenum++; layout.pagenum=pagenum;
			page.id=pageprefix+(pagenum);
			page.setAttribute("data-pagenum",pagenum);
			fdjtDOM(container,page);
			pages.push(page);
			newpage="newpage";}
		    
		    if (trace>1) {
			if (node) logfn("Layout/%s %o at %o",newpage,page,node);
			else logfn("Layout/%s %o",newpage,page);}
		    
		    // If there are things we are dragging along, move
		    // them to the new page
		    if ((drag)&&(drag.length)) {
			var i=0; var lim=drag.length;
			while (i<lim) moveNodeToPage(drag[i++],page,dups);
			layout.drag=drag=[];}
		    // Finally, move the node to the page
		    if (node) moveNodeToPage(node,page,dups);

		    layout.prev=prev=false;
		    return page;}

		// Could this just be the following?
		function fullPage(node){
		    newPage(node);
		    tweakBlock(node);
		    newPage();}

		function fullPage(node){
		    var newpage="fullpagetop";
		    if ((!(page))||
			((!(node))&&(!(hasContent(page,true,true))))||
			(((node)&&(hasParent(node,page)))?
			 (hasContent(page,node,true)):
			 (hasContent(page,true,true)))) {
			if (page) dropClass(page,"curpage");
			layout.page=page=fdjtDOM("div.codexpage.curpage");
			if (!(pagerule)) {
			    page.style.height=page_height+'px';
			    page.style.width=page_width+'px';}
			pagenum++; layout.pagenum=pagenum;
			page.id=pageprefix+(pagenum);
			page.setAttribute("data-pagenum",pagenum);
			fdjtDOM(container,page);
			pages.push(page);
			newpage="newpage";}
		    
		    if (trace>1) {
			if (node) logfj("Layout/%s %o at %o",newpage,page,node);
			else logfn("Layout/%s %o",newpage,page);}
		    
		    if (node) moveNodeToPage(node,page,dups);
		    
		    tweakBlock(node);

		    // Now we make a new page for whatever comes next
		    newPage();

		    layout.prev=prev=false;
		    layout.prevstyle=prevstyle=false;
		    return page;}

		// This gets a little complicated
		function splitBlock(node){
		    if (avoidBreakInside(node)) {
			// Simplest case, if we can't split, we just make a new page
			addClass(node,"codexcantsplit");
			newPage(node);
			return node;}
		    // Otherwise, we remove all of the node's children
		    // and then add them back bit by bit.
		    var children=TOA(node.childNodes);
		    var i=children.length-1;
		    while (i>=0) node.removeChild(children[i--]);
		    var geom=getGeometry(node);
		    if (geom.bottom>page_height) {
			// If the version without any children is
			// already over the edge, just start a new
			// page on the node (after restoring all the
			// children)
			i=0; var n=children.length;
			while (i<n) node.appendChild(children[i++]);
			addClass(node,"codexcantsplit");
			newPage(node);
			return node;}
		    var page_break=node; var i=0; var n=children.length;
		    // We add children back until we go over the edge,
		    //  at which point we'll create a new page.
		    //  page_break is where we need to break If
		    //  page_break is the same as the node we're
		    //  trying to split, it means that we can't split
		    //  this node.
		    while (i<n) {
			var child=children[i++]; var nodetype=child.nodeType;
			// Add the child back and get the geometry
			node.appendChild(child); geom=getGeometry(node);
			if (geom.bottom>page_height) { // Over the edge
			    if ((nodetype!==3)&&(!((hasContent(node,child,true)))))
				// If there's no content before the
				// child (and it's not a text node
				// that can be split), just quit,
				// leaving page_break as the node
				// we're trying to split itself.
				break;
			    else if ((nodetype!==3)&&(nodetype!==1)) {
				// This is probably an error, so stop trying
				break;}
			    // If it's either text or relocated text, try to break it
			    else if ((nodetype===1)&&
				     (!(hasClass(child,"codexwraptext"))))
				// If it's an element, just push it over; this
				// could be more clever for inline elements
				page_break=child;
			    else {
				// If it's text, split it into words,
				// then replace the child with
				// 'probenode's containing
				// progressively longer and longer
				// strings.  It would be nice to use
				// some kind of binary search here,
				// but we want to get the longest
				// possible run of words because we
				// want to avoid ragged lines
				var text=((nodetype===3)?(child.nodeValue):
					  (child.firstChild.nodeValue));
				var words=text.split(/\b/g);
				var probenode=child;
				// If there's only one word, no splitting today,
				//  just push the node itself onto the next page
				if (words.length<2) {page_break=child; break;}
				// Try to find where the page should
				// break by advancing via words and
				// replacing the content of the text
				// node.
				var w=0; var wlen=words.length; var wordstart;
				// We consider all the breaks that
				// don't start with non-word
				// characters (e.g. punctuation, etc).
				while (w<wlen) {
				    wordstart=w++;
				    while ((w<wlen)&&(words[w].search(/\w/)<0)) w++;
				    // The newprobe is a try at a break point of w
				    var newprobe=document.createTextNode(
					words.slice(0,w).join(""));
				    node.replaceChild(newprobe,probenode);
				    probenode=newprobe;
				    geom=getGeometry(node);
				    // If the probe node put us over, break;
				    if (geom.bottom>page_height) break;}
				// We're done searching for the word break
				if ((wordstart===0)||(wordstart===wlen)) {
				    // If we didn't find anything, put
				    // the child back and make it into
				    // the page break
				    node.replaceChild(child,probenode);
				    page_break=child;
				    break;}
				else { // Do the split
				    page_break=document.createTextNode(
					// This is the text pushed onto the new page
					words.slice(wordstart).join(""));
				    var keep=document.createTextNode(
					words.slice(0,wordstart).join(""));
				    // We replace the probe node with 'keep'
				    node.replaceChild(keep,probenode);
				    // And insert the page break after 'keep'
			    	    fdjtDOM.insertAfter(keep,page_break);
				    // Finally, we save texts which
				    // we've split for later restoration
				    if ((nodetype===1)&&
					(child.getAttribute("data-codexorigin"))) {
					var originid=child.getAttribute("data-codexorigin");
					textsplits[originid]=child.firstChild;}}}
			    break;}
			else continue;}
		    // Finally, we create a new page
		    newPage(page_break);
		    if (page_break===node) {
			// If we couldn't find an internal page_break, this node can't be split
			addClass(node,"codexcantsplit");
			if (trace) logfn("Layout/cantBreak %o onto %o",node,page);}
		    else if (page_break!==node) {
			var dup=page_break.parentNode;
			// This (dup) is the copied parent of the page
			// break.  We append all the remaining children
			// to this duplicated parent on the new page.
			while (i<n) dup.appendChild(children[i++]);
			if (trace>1)
			    logfn("Layout/splitBlock %o @ %o into %o on %o",
				  node,page_break,dup,page);}
		    else {}
		    return dup;}

		// This uses lots of tricks (mostly CSS3) to get a
		// page to fit after subdivision based layout which
		// hasn't yielded adequate results
		function tweakBlock(node,avail_width,avail_height){
		    if (hasClass(node,"codextweaked")) return;
		    else if (hasClass(node,"codexavoidtweak")) return;
		    else if (node.getAttribute("style")) {
			addClass(node,"codexavoidtweak");
			return;}
		    else addClass(node,"codextweaked");
		    var geom=getGeometry(node,page,true);
		    if (!((avail_width)&&(avail_height))) {
			var h_margin=(geom.left_margin+geom.right_margin);
			var v_margin=(geom.top_margin+geom.bottom_margin);
			if (!(avail_width))
			    avail_width=page_width-(geom.left_margin+geom.right_margin+geom.left);
			if (!(avail_height))
			    avail_height=page_height-(geom.top_margin+geom.bottom_margin+geom.top);}
		    // If the node doesn't have any dimensions,
		    //  something hasn't loaded, so don't try tweaking
		    if ((geom.width===0)||(geom.height===0)) return;
		    var scalex=(avail_width/geom.width);
		    var scaley=(avail_height/geom.height);
		    var scale=((scalex<scaley)?(scalex):(scaley));
		    // Try a CSS scale transform
		    node.style[fdjtDOM.transform]='scale('+scale+','+scale+')';
		    var ngeom=getGeometry(node,page,true);
		    if (ngeom.height===geom.height) {
			if ((node.tagName==='IMG')||(node.tagName==='img')) {
			    node.style[fdjtDOM.transform]='';
			    scaleImage(node,scale,geom);}
			else {
			    // If that didn't work, try some tricks
			    var images=fdjtDOM.getChildren(node,"IMG");
			    if ((images)&&(images.length)) {
				var j=0; var jlim=images.length;
				while (j<jlim) {
				    var img=images[j++];
				    if (hasClass(img,/\bcodextweaked\b/)) continue;
				    else if (hasClass(img,/\bcodexavoidtweak\b/))
					continue;
				    else if ((img.getAttribute('style'))||
					     (img.getAttribute('width'))||
					     (img.getAttribute('height'))) {
					addClass(img,'codexavoidtweak');
					continue;}
				    var igeom=getGeometry(img,page);
				    // If the node doesn't have any
				    //  dimensions, it hasn't been loaded,
				    //  so don't try tweaking the page
				    if ((igeom.width===0)||(igeom.height===0))
					return;
				    var relscale=Math.max(
					igeom.width/geom.width,
					igeom.height/geom.height);
				    scaleImage(img,scale*relscale,igeom);}}}
			ngeom=getGeometry(node,page,true);
			if (ngeom.height===geom.height)
			    addClass(node,"codexuntweakable");}
		    addClass(node,"codextweaked");}

		
		loop(root);
		
		return this;}
	    this.addContent=addContent;

	    /* Finishing the page */

	    function finishPage(completed) {
		var bounds=insideBounds(completed);
		if (!((page_width)&&(page_height))) {
		    var geom=getGeometry(completed);
		    if (!(page_width)) page_width=geom.width;
		    if (!(page_height)) page_height=geom.height;}
		if ((bounds.width>page_width)||(bounds.height>page_height)) {
		    var scaled=fdjt$(".codexscale",completed);
		    if ((scaled)&&(scaled.length)) {
			// To be implemented: try adjusting these elements first
			bounds=insideBounds(completed);}
		    if ((bounds.width>page_width)||(bounds.height>page_height)) {
			var boxed=fdjtDOM("div",completed.childNodes);
			var scalex=page_width/bounds.width;
			var scaley=page_height/bounds.height;
			var scale=((scalex<scaley)?(scalex):(scaley));
			var transform='scale('+scale+','+scale+')';
			boxed.style[fdjtDOM.transform]=transform;
			boxed.style[fdjtDOM.transform+"-origin"]='top';
			completed.appendChild(boxed);}}
		if (this.pagedone) this.pagedone(completed);
		dropClass(completed,"curpage");}
	    this.finishPage=finishPage;

	    /* Finishing the overall layout */

	    function Finish(){
		for (dupid in dups) {
		    var dup=dups[dupid];
		    dup.className=dup.className.replace(
			    /\bcodexdup\b/,"codexdupend");}
		dropClass(page,"curpage");
		var i=0; var lim= pages.length;
		while (i<lim) finishPage(pages[i++]);
		layout.done=fdjtTime();}
	    this.Finish=Finish;

	    /* page break predicates */
	    
	    function forcedBreakBefore(elt,style){
		if (!(elt)) return false;
		if (!(style)) style=getStyle(elt);
		return (style.pageBreakBefore==='always')||
		    (hasClass(elt,"forcebreakbefore"))||
		    ((forcebreakbefore)&&(testNode(elt,forcebreakbefore)));}
	    this.forcedBreakBefore=forcedBreakBefore;
	    
	    function forcedBreakAfter(elt,style){ 
		if (!(elt)) return false;
		if (!(style)) style=getStyle(elt);
		return (style.pageBreakAfter==='always')||
		    (hasClass(elt,"forcebreakafter"))||
		    ((forcebreakafter)&&(testNode(elt,forcebreakafter)));}
	    this.forcedBreakAfter=forcedBreakAfter;

	    // We explicitly check for these classes because some browsers
	    //  which should know better (we're looking at you, Firefox) don't
	    //  represent (or handle) page-break 'avoid' values.  Sigh.
	    var page_block_classes=/\b(avoidbreakinside)|(sbookpage)\b/;
	    function avoidBreakInside(elt,style){
		if (!(elt)) return false;
		if (elt.tagName==='IMG') return true;
		if (!(style)) style=getStyle(elt);
		return (style.pageBreakInside==='avoid')||
		    ((elt.className)&&(elt.className.search(page_block_classes)>=0))||
		    ((avoidbreakinside)&&(testNode(elt,avoidbreakinside)));}
	    this.avoidBreakInside=avoidBreakInside;
	    
	    function avoidBreakBefore(elt,style){
		if (!(elt)) return false;
		if (!(style)) style=getStyle(elt);
		var info=((elt.id)&&(Codex.docinfo[elt.id]));
		return ((style.pageBreakBefore==='avoid')||
			(hasClass(elt,"abovebreakbefore"))||
			((avoidbreakbefore)&&(testNode(elt,avoidbreakbefore))));}
	    this.avoidBreakBefore=avoidBreakBefore;

	    function avoidBreakAfter(elt,style){
		if (!(elt)) return false;
		if (!(style)) style=getStyle(elt);
		if (style.pageBreakAfter==='avoid') return true;
		else if ((style.pageBreakAfter)&&(style.pageBreakAfter!=="auto"))
		    return false;
		else return ((avoidbreakafter)&&(testNode(elt,avoidbreakafter)));}
	    this.avoidBreakAfter=avoidBreakAfter;
	    
	    function getPage(spec) {
		if (!(spec)) return false;
		else if (typeof spec === 'number')
		    return fdjtID(pageprefix+spec);
		else if (spec.nodeType) {
		    if (hasClass(spec,"codexpage")) return spec;
		    else return getParent(spec,".codexpage");}
		else if (typeof spec === "string")
		    return getPage(fdjtID(spec));
		else {
		    logfn("Can't determine page from %o",spec);
		    return false;}}
	    this.getPage=getPage;

	    function getDup(node,page){
		if (typeof node === 'string')
		    node=document.getElementById(node);
		if (!(node)) return false;
		if (hasParent(node,page)) return node;
		var nodeid=node.id;
		var duptable=Codex.paginated.dups;
		var dups=duptable[nodeid];
		var i=0; var lim=dups.length;
		while (i<lim) {
		    if (hasParent(dups[i]),page) return dups[i];
		    else i++;}
		return false;}
	    this.getDup=getDup;

	    function gotoPage(spec) {
		var newpage=false;
		if (!(spec)) return false;
		else if (typeof spec === 'number')
		    newpage=fdjtID(pageprefix+spec);
		else if (spec.nodeType) {
		    if (hasClass(spec,"codexpage")) newpage=spec;
		    else newpage=getParent(spec,".codexpage");}
		else if (typeof spec === "string")
		    newpage=getPage(fdjtID(spec));
		else {
		    logfn("Can't determine page from %o",spec);
		    return false;}
		if (!(newpage)) return;
		var oldpage=container.getChildrenByClassName('curpage');
		dropClass(oldpage,"curpage");
		addClass(newpage,"curpage");}
	    this.gotoPage=gotoPage;

	    this.Revert=function(){revertLayout(this);};

	    /* Finally return the layout */

	    return this;}

	CodexLayout.tracelevel=0;
	
	return CodexLayout;})();


/* Mini Manual */
/*
  var layout=new CodexLayout();
  layout.addContent(node);
  layout.Finish();
  layout.Revert();

  var layout=new CodexLayout({
  page_width: 500, page_height: 500, // Dimensions
  // Where to add new pages; by default this creates a
  //  new div#CODEXPAGES.codexpages at the bottom of the BODY
  container: document.getElementByID("MYPAGES"),
  // Prefix for page element IDs, e.g. page 42 would have id MYCODEXPAGE42
  pageprefix: "MYCODEXPAGE",
  logfn: console.log, // how to log notable events
  // Layout rules:
  // Always put H1 elements on a new page
  forcebreakbefore: "H1",
  // Always follow div.signature with a page break
  forcebreakafter: "div.signature",
  // Avoid breaking inside
  avoidbreakinside: "div.code",
  // Avoid breaking before these elements
  avoidbreakbefore: "div.signature,div.attribution",
  // Avoid breaking after these elements
  avoidbreakafter: "h1,h2,h3,h4,h5,h6,h7",
  // Put this element on a page by itself
  codexfullpage: "div.titlepage",
  // Put this element on a page by itself, but don't interrupt the
  // narrative flow
  codexfloatpage: "div.illustration"});
*/

/* Emacs local variables
   ;;;  Local variables: ***
   ;;;  compile-command: "cd ..; make" ***
   ;;;  End: ***
*/
/* -*- Mode: Javascript; Character-encoding: utf-8; -*- */

/* Copyright (C) 2009-2011 beingmeta, inc.
   This file implements a Javascript/DHTML UI for reading
   large structured documents (sBooks).

   For more information on sbooks, visit www.sbooks.net
   For more information on knodules, visit www.knodules.net
   For more information about beingmeta, visit www.beingmeta.com

   This library uses the FDJT (www.fdjt.org) toolkit.

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

   Use and redistribution (especially embedding in other
   CC licensed content) is permitted under the terms of the
   Creative Commons "Attribution-NonCommercial" license:

   http://creativecommons.org/licenses/by-nc/3.0/ 

   Other uses may be allowed based on prior agreement with
   beingmeta, inc.  Inquiries can be addressed to:

   licensing@beingmeta.com

   Enjoy!

*/

var Codex=
    {mode: false,hudup: false,scrolling: false,query: false,
     head: false,target: false,glosstarget: false,location: false,
     user: false,root: false,start: false,HUD: false,dosync: true,
     _setup: false,_user_setup: false,_gloss_setup: false,_social_setup: false,
     // Keeping track of paginated context
     curpage: false,curoff: false,curinfo: false, curbottom: false,
     // For tracking UI state
     last_mode: false, last_flyleaf: "about",
     // How long it takes a gesture to go from tap to hold
     holdmsecs: 500, edgeclick: 50, pagesize: 250,
     animate: {pages:true,hud: true}, // colbreak: true,
     glossmodes: /(addtag)|(addlink)|(excerpt)|(editnote)/,
     updatehash: true,
     // This is the base URI for this document, also known as the REFURI
     // A document (for instance an anthology or collection) may include
     // several refuri's, but this is the default.
     refuri: false,
     // These are the refuris used in this document
     refuris: [],
     // This is the document URI, which is usually the same as the REFURI.
     docuri: false,
     // This is the unique signed DOC+USER identifier used by myCopy
     // social DRM
     mycopyid: false, 
     // This is the time of the last update
     syncstamp: false,
     // Whether to use native scrolling for body content
     nativescroll: false,
     // Whether to use native scrolling for embedded DIVs
     scrolldivs: true,
     // Dominant interaction mode
     mouse: true,touch: false,kbd: false,
     // Restrictions on excerpts
     min_excerpt: 3, max_excerpt: false,
     // How many past page layouts to cache
     cachelayouts: 0,
     // Various handlers, settings, and status information for the
     // Codex interface
     UI: {
	 // This maps device types into sets of node->event handlers
	 handlers: {mouse: {}, kbd: {}, ios: {}}},
     Debug: {},
     Trace: {
	 startup: 1,	// Whether to debug startup
	 config: false,  // Whether to trace config setup/modification/etc
	 mode: false,	// Whether to trace mode changes
	 nav: false,	// Whether to trace book navigation
	 scan: false,	// Whether to trace DOM scanning
	 search: 0,	// How much to trace searches
	 clouds: 0,	// How much to trace cloud generation
	 focus: false,	// Whether to trace target changes
	 toc: false,	// Whether we're debugging TOC tracking
	 network: 0,	// How much to trace server interaction
	 glosses: false,// Whether we're tracing gloss processing
	 offline: 0,    // Whether to trace offline restoration, etc
	 layout: 0,	// How much to trace pagination
	 dosync: false, // Whether to trace state saves
	 flips: false,	// Whether to trace page flips (movement by pages)
	 scroll: false,	// Whether to trace scrolling within the HUD
	 gestures: 0}   // How much to trace gestures
    };
var _sbook_setup=false;

var CodexHUD=false;

var sbook_gloss_data=
    ((typeof sbook_gloss_data === 'undefined')?(false):
     (sbook_gloss_data));

(function(){

    function initDB() {
	if (Codex.Trace.start>1) fdjtLog("Initializing DB");
	var refuri=(Codex.refuri||document.location.href);
	if (refuri.indexOf('#')>0) refuri=refuri.slice(0,refuri.indexOf('#'));
	var docinfo=Codex.DocInfo=new fdjtKB.Pool(refuri+"#");
	fdjtKB.addRefMap(docinfo.map);
	fdjtKB.addRefMap(function(ref){
	    return ((typeof ref === 'string')&&(ref[0]==='#')&&
		    (docinfo.ref(ref.slice(1))));});
	
	var knodule_name=
	    fdjtDOM.getMeta("codex.knodule")||
	    fdjtDOM.getMeta("KNODULE")||
	    refuri;
	Codex.knodule=new Knodule(knodule_name);
	Codex.index=new KnoduleIndex(Codex.knodule);
	Codex.query=Codex.empty_query=Codex.index.Query([]);
	Codex.BRICO=new Knodule("BRICO");
	Codex.BRICO.addAlias(":@1/");
	Codex.glosses=new fdjtKB.Pool("glosses"); {
	    var superadd=Codex.glosses.add;
	    Codex.glosses.addAlias("glossdb");
	    Codex.glosses.addAlias("-UUIDTYPE=61");
	    Codex.glosses.addAlias(":@31055/");
	    Codex.glosses.xforms['tags']=function(tag){
		if (typeof tag==='string') {
		    var info=
			((tag.indexOf('|')>=0)?
			 (Codex.knodule.handleSubjectEntry(tag)):
			 (fdjtKB.ref(tag)));
		    if (info) return info.tagString(Codex.knodule);
		    else return tag;}
		else return tag.tagString(Codex.knodule);};
	    Codex.glosses.addInit(function(item) {
		var info=Codex.docinfo[item.frag];
		if (!(info))
		    fdjtLog("Gloss refers to nonexistent '%s': %o",
			    item.frag,item);
		if ((info)&&(info.starts_at)) {item.starts_at=info.starts_at;}
		if ((info)&&(info.starts_at)) {item.ends_at=info.ends_at;}
		Codex.index.add(item,item.maker);
		Codex.addTag2UI(item.maker);
		var tags=item.tags;
		if (tags) {
		    if (!(tags instanceof Array)) tags=[tags];
		    if ((tags)&&(tags.length)) {
			var i=0; var lim=tags.length;
			while (i<lim) {
			    var tag=tags[i++]; var score=false;
			    if (tag[0]==='*') {
				score=tag.search(/[^*]/);
				tag=tag.slice(score);}
			    var knode=fdjtKB.ref(tag)||tag;
			    if (info.glosstags)
				info.glosstags.push(knode);
			    else info.glosstags=[knode];
			    Codex.index.add(item,knode);
			    Codex.addTag2UI(knode,true);}}}
		var sources=item.sources;
		if (sources) {
		    if (typeof sources === 'string') sources=[sources];
		    if ((sources)&&(sources.length)) {
			var i=0; var lim=sources.length;
			while (i<lim) {
			    var source=sources[i++];
			    Codex.index.add(item,source);
			    Codex.UI.addGlossSource(fdjtKB.ref(source),true);}}}});
	    Codex.glosses.index=new fdjtKB.Index();
	    if (Codex.offline)
		Codex.glosses.storage=new fdjtKB.OfflineKB(Codex.glosses);}
	Codex.sourcekb=new fdjtKB.Pool("sources");{
	    Codex.sourcekb.addAlias("@1961/");
	    Codex.sourcekb.index=new fdjtKB.Index();
	    if (Codex.offline)
		Codex.sourcekb.storage=new fdjtKB.OfflineKB(Codex.sourcekb);}
	if (Codex.Trace.start>1) fdjtLog("Initialized DB");}
    Codex.initDB=initDB;

    var trace1="%s %o in %o: mode%s=%o, target=%o, head=%o scanning=%o";
    var trace2="%s %o: mode%s=%o, target=%o, head=%o scanning=%o";
    function sbook_trace(handler,cxt){
	var target=((cxt.nodeType)?(cxt):(fdjtUI.T(cxt)));
	if (target)
	    fdjtLog(trace1,handler,cxt,target,
		    ((Codex.scanning)?("(scanning)"):""),Codex.mode,
		    Codex.target,Codex.head,Codex.scanning);
	else fdjtLog(trace2,handler,cxt,
		     ((Codex.scanning)?("(scanning)"):""),Codex.mode,
		     Codex.target,Codex.head,Codex.scanning);}
    Codex.trace=sbook_trace;

    // This is the hostname for the sbookserver.
    Codex.server=false;
    // Whether this sbook is set up for offline reading
    Codex.offline=false;
    // This is an array for looking up sbook servers.
    Codex.servers=[[/.sbooks.net$/g,"gloss.sbooks.net"]];
    //Codex.servers=[];
    // This is the default server
    Codex.default_server="gloss.sbooks.net";
    // There be icons here!
    function sbicon(name,suffix) {return Codex.graphics+name+(suffix||"");}
    function cxicon(name,suffix) {
	return Codex.graphics+"codex/"+name+(suffix||"");}
    Codex.graphics="http://static.beingmeta.com/graphics/";
    // Codex.graphics="https://www.sbooks.net/static/graphics/";
    // Codex.graphics="https://beingmeta.s3.amazonaws.com/static/graphics/";

    Codex.getRefURI=function(target){
	var scan=target;
	while (scan)
	    if (scan.refuri) return scan.refuri;
	else scan=scan.parentNode;
	return Codex.refuri;}

    Codex.getDocURI=function(target){
	var scan=target;
	while (scan) {
	    var docuri=
		(((scan.getAttributeNS)&&
		  (scan.getAttributeNS("docuri","http://sbooks.net/")))||
		 ((scan.getAttribute)&&(scan.getAttribute("docuri")))||
		 ((scan.getAttribute)&&(scan.getAttribute("data-docuri"))));
	    if (docuri) return docuri;
	    else scan=scan.parentNode;}
	return Codex.docuri;}

    Codex.getRefID=function(target){
	if (target.getAttributeNS)
	    return (target.getAttributeNS('sbookid','http://sbooks.net/'))||
	    (target.getAttributeNS('sbookid'))||
	    (target.getAttributeNS('data-sbookid'))||
	    (target.id)||(target.codexdupid);
	else return target.id;};

    function getHead(target){
	/* First, find some relevant docinfo */
	var targetid=(target.id)||(target.codexdupid);
	if ((targetid)&&(Codex.docinfo[targetid]))
	    target=Codex.docinfo[targetid];
	else if (targetid) {
	    while (target)
		if ((target.id)&&(Codex.docinfo[targetid])) {
		    target=Codex.docinfo[targetid]; break;}
	    else target=target.parentNode;}
	else {
	    /* First, try scanning forward to find a non-empty node */
	    var scan=target.firstChild; var scanid=false; var next=target.nextNode;
	    while ((scan)&&(scan!=next)) {
		if ((scan.id)||(scan.codexdupid)) break;
		if ((scan.nodeType===3)&&
		    (!(fdjtString.isEmpty(scan.nodeValue)))) break;
		scan=fdjtDOM.forward(scan);}
	    /* If you found something, use it */
	    if ((scan)&&(scan.id)&&(scan!=next))
		target=Codex.docinfo[scanid];
	    else {
		while (target)
		    if ((targetid=((target.id)||(target.codexdupid)))&&
			(Codex.docinfo[targetid])) {
			target=Codex.docinfo[targetid]; break;}
		else target=target.parentNode;}}
	if (target)
	    if (target.level)
		return target.elt||document.getElementById(target.frag);
	else if (target.head)
	    return target.head.elt||
	    document.getElementById(target.head.frag);
	else return false;
	else return false;}
    Codex.getHead=getHead;

    Codex.getRef=function(target){
	while (target)
	    if (target.about) break;
	else if ((target.getAttribute)&&(target.getAttribute("about"))) break;
	else target=target.parentNode;
	if (target) {
	    var ref=((target.about)||(target.getAttribute("about")));
	    if (!(target.about)) target.about=ref;
	    if (ref[0]==='#')
		return document.getElementById(ref.slice(1));
	    else return document.getElementById(ref);}
	else return false;}
    Codex.getRefElt=function(target){
	while (target)
	    if ((target.about)||
		((target.getAttribute)&&(target.getAttribute("about"))))
		break;
	else target=target.parentNode;
	return target||false;}

    Codex.checkTarget=function(){
	if ((Codex.target)&&(Codex.mode==='glosses'))
	    if (!(fdjtDOM.isVisible(Codex.target))) {
		CodexMode(false); CodexMode(true);}};

    function getTarget(scan,closest){
	scan=scan.target||scan.srcElement||scan;
	var target=false;
	while (scan) {
	    if (scan.codexui) return false;
	    else if (scan===Codex.root) return target;
	    else if ((scan.id)||(scan.codexdupid)) {
		if (fdjtDOM.hasParent(scan,CodexHUD)) return false;
		else if (fdjtDOM.hasParent(scan,".codexmargin")) return false;
		else if ((fdjtDOM.hasClass(scan,"sbooknofocus"))||
			 ((Codex.nofocus)&&(Codex.nofocus.match(scan))))
		    scan=scan.parentNode;
		else if ((fdjtDOM.hasClass(scan,"sbookfocus"))||
			 ((Codex.focus)&&(Codex.focus.match(scan))))
		    return scan;
		else if (!(fdjtDOM.hasText(scan)))
		    scan=scan.parentNode;
		else if (closest) return scan;
		else if (target) scan=scan.parentNode;
		else {target=scan; scan=scan.parentNode;}}
	    else scan=scan.parentNode;}
	return target;}
    Codex.getTarget=getTarget;
    
    Codex.getTitle=function(target,tryhard) {
	var targetid;
	return target.sbooktitle||
	    (((targetid=(target.id||(target.codexdupid)))&&
	      (Codex.docinfo[targetid]))?
	     (Codex.docinfo[targetid].title):
	     (target.title))||
	    ((tryhard)&&
	     (fdjtDOM.textify(target)).
	     replace(/\n\n+/g,"\n").
	     replace(/^\n+/,"").
	     replace(/\n+$/,"").
	     replace(/\n+/g," // "));};

    function getinfo(arg){
	if (arg)
	    if (typeof arg === 'string')
		return Codex.docinfo[arg]||fdjtKB.ref(arg,Codex.glosses)||fdjtKB.ref(arg);
	else if (arg._id) return arg;
	else if (arg.id) return Codex.docinfo[arg.id];
	else if (arg.codexdupid)
	    return Codex.docinfo[arg.codexdupid];
	else return false;
	else return false;}
    Codex.Info=getinfo;

    /* Navigation functions */

    function setHead(head){
	if (head===null) head=Codex.root;
	else if (typeof head === "string") 
	    head=getHead(fdjtID(head))||Codex.root;
	else head=getHead(head)||Codex.root;
	var headid=head.id||head.codexdupid;
	var headinfo=Codex.docinfo[headid];
	if (!(head)) return;
	else if (head===Codex.head) {
	    if (Codex.Trace.focus) fdjtLog("Redundant SetHead");
	    return;}
	else if (head) {
	    if (Codex.Trace.focus) Codex.trace("Codex.setHead",head);
	    CodexTOC.update("CODEXTOC4",headinfo,fdjtID("CODEXTOC"));
	    CodexTOC.update("CODEXFLYTOC4",headinfo,fdjtID("CODEXFLYTOC"));
	    window.title=headinfo.title+" ("+document.title+")";
	    if (Codex.head) fdjtDOM.dropClass(Codex.head,"sbookhead");
	    fdjtDOM.addClass(head,"sbookhead");
	    Codex.setLocation(Codex.location);
	    Codex.head=fdjtID(headid);}
	else {
	    if (Codex.Trace.focus) Codex.trace("Codex.setHead",head);
	    CodexTOC.update(head,"CODEXTOC4");
	    CodexTOC.update(head,"CODEXFLYTOC4");
	    Codex.head=false;}}
    Codex.setHead=setHead;

    function setLocation(location,force){
	if ((!(force)) && (Codex.location===location)) return;
	if (Codex.Trace.toc)
	    fdjtLog("Setting location to %o",location);
	var info=Codex.Info(Codex.head);
	while (info) {
	    var tocelt=document.getElementById("CODEXTOC4"+info.frag);
	    var flytocelt=document.getElementById("CODEXFLYTOC4"+info.frag);
	    var start=tocelt.sbook_start; var end=tocelt.sbook_end;
	    var progress=((location-start)*100)/(end-start);
	    var bar=fdjtDOM.getFirstChild(tocelt,".progressbar");
	    var appbar=fdjtDOM.getFirstChild(flytocelt,".progressbar");
	    tocelt.title=flytocelt.title=Math.round(progress)+"%";
	    if (Codex.Trace.toc)
		fdjtLog("For tocbar %o loc=%o start=%o end=%o progress=%o",
			bar,location,start,end,progress);
	    if ((bar)&& (progress>=0) && (progress<=100)) {
		// bar.style.width=((progress)+10)+"%";
		// appbar.style.width=((progress)+10)+"%";
		bar.style.width=(progress)+"%";
		appbar.style.width=(progress)+"%";
	    }
	    info=info.head;}
	var spanbars=fdjtDOM.$(".spanbar");
	var i=0; while (i<spanbars.length) {
	    var spanbar=spanbars[i++];
	    var width=spanbar.ends-spanbar.starts;
	    var ratio=(location-spanbar.starts)/width;
	    if (Codex.Trace.toc)
		fdjtLog("ratio for spanbar %o[%d] is %o [%o,%o,%o]",
			spanbar,spanbar.childNodes[0].childNodes.length,
			ratio,spanbar.starts,location,spanbar.ends);
	    if ((ratio>=0) && (ratio<=1)) {
		var progressbox=fdjtDOM.$(".progressbox",spanbar);
		if (progressbox.length>0) {
		    progressbox[0].style.left=((Math.round(ratio*10000))/100)+"%";}}}
	Codex.location=location;}
    Codex.setLocation=setLocation;

    function setTarget(target,nogo,nosave){
	if (Codex.Trace.focus) Codex.trace("Codex.setTarget",target);
	if (target===Codex.target) return;
	else if ((!target)&&(Codex.target)) {
	    fdjtDOM.dropClass(Codex.target,"sbooktarget");
	    Codex.target=false;
	    return;}
	else if (!(target)) return;
	else if ((inUI(target))||(!(target.id||target.codexdupid)))
	    return;
	else if ((target===Codex.root)||(target===Codex.body)||
		 (target===document.body)) {
	    if (!(nogo)) Codex.GoTo(target,true);
	    return;}
	if (Codex.target) {
	    fdjtDOM.dropClass(Codex.target,"sbooktarget");
	    Codex.target=false;}
	fdjtDOM.addClass(target,"sbooktarget");
	fdjtState.setCookie("sbooktarget",target.id||target.getAttribute('data-sbookid'));
	Codex.target=target;
	if (Codex.full_cloud)
	    Codex.setCloudCuesFromTarget(Codex.full_cloud,target);
	if (!(nosave))
	    setState({target: target.id||target.getAttribute('data-sbookid'),
		      location: Codex.location,
		      page: Codex.curpage});
 	if (!(nogo)) Codex.GoTo(target,true);}
    Codex.setTarget=setTarget;

    /* Navigation */

    function resizeBody(){
	if (Codex.nativescroll) {}
	else {
	    var curx=x_offset-fdjtDOM.parsePX(Codex.pages.style.left);
	    var cury=y_offset-fdjtDOM.parsePX(Codex.pages.style.top);
	    // Codex.body.style.left=''; Codex.body.style.top='';
	    var geom=fdjtDOM.getGeometry(Codex.body,Codex.body);
	    x_offset=geom.left; y_offset=geom.top;
	    Codex.bodyoff=[x_offset,y_offset];
	    Codex.pages.style.left='0px';
	    Codex.pages.style.top=(y_offset)+'px';}}
    Codex.resizeBody=resizeBody;

    Codex.viewTop=function(){
	if (Codex.nativescroll) return fdjtDOM.viewTop();
	else return -(fdjtDOM.parsePX(Codex.pages.style.top));}
    var sbookUIclasses=
	/(\bhud\b)|(\bglossmark\b)|(\bleading\b)|(\bcodexmargin\b)/;

    function inUI(elt){
	if (elt.codexui) return true;
	else if (fdjtDOM.hasParent(elt,CodexHUD)) return true;
	else while (elt)
	    if (elt.codexui) return true;
	else if (fdjtDOM.hasClass(elt,sbookUIclasses)) return true;
	else elt=elt.parentNode;
	return false;}

    function displayOffset(){
	var toc;
	if (Codex.mode)
	    if (toc=fdjtID("CODEXTOC"))
		return -((toc.offsetHeight||50)+15);
	else return -60;
	else return -40;}

    function setHashID(target){
	var targetid=target.id||target.codexdupid;
	if ((!(targetid))||(window.location.hash===targetid)||
	    ((window.location.hash[0]==='#')&&
	     (window.location.hash.slice(1)===targetid)))
	    return;
	if ((target===Codex.body)||(target===document.body)) return;
	window.location.hash=targetid;}
    Codex.setHashID=setHashID;

    var syncing=false;
    
    function setState(state){
	if ((Codex.state===state)||
	    ((Codex.state)&&
	     (Codex.state.target===state.target)&&
	     (Codex.state.location===state.location)&&
	     (Codex.state.page===state.page)))
	    return;
	if (syncing) return;
	if (!(Codex.dosync)) return;
	if (!(state.tstamp)) state.tstamp=fdjtTime.tick();
	if (!(state.refuri)) state.refuri=Codex.refuri;
	Codex.state=state;
	var statestring=JSON.stringify(state);
	var uri=Codex.docuri||Codex.refuri;
	fdjtState.setLocal("codex.state("+uri+")",statestring);}
    Codex.setState=setState;
    
    function serverSync(){
	if ((Codex.user)&&(Codex.dosync)&&(navigator.onLine)) {
	    var state=Codex.state; var synced=Codex.syncstate;
	    // Warning when syncing doesn't return?
	    if (syncing) return;
	    if (!(state)) {
		var uri=Codex.docuri||Codex.refuri;
		var statestring=fdjtState.getLocal("codex.state("+uri+")");
		if (statestring) Codex.state=state=JSON.parse(statestring);
		else state={};}
	    if ((synced)&&
		(synced.target===state.target)&&
		(synced.location===state.location)&&
		(synced.page===state.page))
		return;
	    var refuri=((Codex.target)&&(Codex.getRefURI(Codex.target)))||
		(Codex.refuri);
	    var uri="https://"+Codex.server+"/v1/sync?ACTION=save"+
		"&DOCURI="+encodeURIComponent(Codex.docuri)+
		"&REFURI="+encodeURIComponent(refuri);
	    if (Codex.deviceId)
		uri=uri+"&deviceid="+encodeURIComponent(Codex.deviceId);
	    if (Codex.deviceName)
		uri=uri+"&devicename="+encodeURIComponent(Codex.deviceName);
	    if (state.target) uri=uri+"&target="+encodeURIComponent(state.target);
	    if ((state.location)||(state.hasOwnProperty('location')))
		uri=uri+"&location="+encodeURIComponent(state.location);
	    if (Codex.ends_at) uri=uri+"&maxloc="+encodeURIComponent(Codex.ends_at);
	    if ((state.page)||(state.hasOwnProperty('page')))
		uri=uri+"&page="+encodeURIComponent(state.page);
	    if (typeof Codex.pagecount === 'number')
		uri=uri+"&maxpage="+encodeURIComponent(Codex.pagecount);
	    if (Codex.Trace.dosync)
		fdjtLog("syncPosition(call) %s: %o",uri,state);
	    var req=new XMLHttpRequest();
	    syncing=state;
	    req.onreadystatechange=function(evt){
		if ((req.readyState===4)&&(req.status>=200)&&(req.status<300)) {
		    Codex.syncstate=syncing;
		    syncing=false;}
		if (Codex.Trace.dosync)
		    fdjtLog("serverSync(callback) ready=%o status=%o %o",
			    req.readyState,req.status,evt);};
	    req.open("GET",uri,true);
	    req.withCredentials='yes';
	    req.send();}}
    Codex.serverSync=serverSync;

    function scrollToElt(elt,cxt){
	if ((elt.getAttribute) &&
	    ((elt.tocleve)|| (elt.getAttribute("toclevel")) ||
	     ((elt.sbookinfo) && (elt.sbookinfo.level))))
	    setHead(elt);
	else if (elt.head)
	    setHead(elt.head);
	if (Codex.paginate)
	    Codex.GoToPage(elt,"scrollTo");
	else if (fdjtDOM.isVisible(elt)) {}
	else if ((!cxt) || (elt===cxt))
	    fdjtUI.scrollIntoView(elt,elt.id,false,true,displayOffset());
	else fdjtUI.scrollIntoView(elt,elt.id,cxt,true,displayOffset());}
    
    function getLocInfo(elt){
	var eltid=false;
	var counter=0; var lim=200;
	var forward=fdjtDOM.forward;
	while ((elt)&&(counter<lim)) {
	    eltid=elt.id||elt.codexdupid;
	    if ((eltid)&&(Codex.docinfo[eltid])) break;
	    else {counter++; elt=forward(elt);}}
	if ((eltid)&&(Codex.docinfo[eltid])) {
	    var info=Codex.docinfo[eltid];
	    return {start: info.starts_at,end: info.ends_at,
		    len: info.ends_at-info.starts_at};}
	else return false;}
    Codex.getLocInfo=getLocInfo;

    function resolveLocation(loc){
	var allinfo=Codex.docinfo._allinfo;
	var i=0; var lim=allinfo.length;
	while (i<lim) {
	    if (allinfo[i].starts_at<loc) i++;
	    else break;}
	while (i<lim)  {
	    if (allinfo[i].starts_at>loc) break;
	    else i++;}
	return fdjtID(allinfo[i-1].frag);}
    Codex.resolveLocation=resolveLocation;

    
    // This moves within the document in a persistent way
    function CodexGoTo(arg,noset,nosave){
	var target; var location;
	if (typeof arg === 'string') {
	    target=document.getElementById(arg);
	    var info=getLocInfo(target);
	    location=info.start;}
	else if (typeof arg === 'number') {
	    location=arg;
	    target=resolveLocation(arg);}
	else if (arg.nodeType) {
	    var info=getLocInfo(arg);
	    if (arg.id) target=arg;
	    else if (arg.codexdupid) target=fdjtID(arg.codexdupid);
	    else target=getTarget(arg);
	    location=info.start;}
	else {
	    fdjtLog.warn("Bad CodexGoTo %o",arg);
	    return;}
	if (!(target)) {
	    if (Codex.paginated) Codex.GoToPage(arg);
	    return;}
	var page=((Codex.paginate)&&
		  (Codex.pagecount)&&
		  (Codex.getPage(target)));
	var targetid=target.id||target.codexdupid;
	var info=((targetid)&&(Codex.docinfo[targetid]));
	if (Codex.Trace.nav)
	    fdjtLog("Codex.GoTo() #%o@P%o/L%o %o",
		    targetid,page,((info)&&(info.starts_at)),target);
	if ((targetid)&&(Codex.updatehash))
	    setHashID(target);
	if (info) {
	    if (typeof info.level === 'number')
		setHead(target);
	    else if (info.head) setHead(info.head.frag);
	    else setHead(false);}
	else setHead(false);
	setLocation(location);
	if ((!(noset))&&(targetid)&&(!(inUI(target))))
	    setTarget(target,true,nosave||false);
	if (nosave) {}
	else if (noset)
	    Codex.setState({
		target: ((Codex.target)&&(Codex.targetid)),
		location: location,page: page})
	else Codex.setState(
	    {target: (targetid),location: location,page: page});
	if (page) Codex.GoToPage(target,"CodexGoTo",nosave||false);
	Codex.location=location;}
    Codex.GoTo=CodexGoTo;
    
    function anchorFn(evt){
	var target=fdjtUI.T(evt);
	while (target)
	    if (target.href) break; else target=target.parentNode;
	if ((target)&&(target.href)&&(target.href[0]==='#')) {
	    var elt=document.getElementById(target.href.slice(1));
	    if (elt) {CodexGoTo(elt); fdjtUI.cancel(evt);}}}
    Codex.anchorFn=anchorFn;

    // This jumps and disables the HUD at the same time
    function CodexJumpTo(target){
      if (Codex.hudup) CodexMode(false);
      CodexGoTo(target);}
    Codex.JumpTo=CodexJumpTo;

    // This jumps and disables the HUD at the same time
    // We try to animate the transition
    function CodexScanTo(target){
	if (Codex.hudup) { // Figure out what mode to go to
	    var headinfo=Codex.docinfo[target]||Codex.docinfo[target.id];
	    if ((headinfo)&&((!(headinfo.sub))||(headinfo.sub.length===0)))
		CodexMode("tocscan");}
	CodexGoTo(target);}
    Codex.ScanTo=CodexScanTo;

    function getLevel(elt){
	if (elt.toclevel) {
	    if (elt.toclevel==='none')
		return elt.toclevel=false;
	    else return elt.toclevel;}
	var attrval=
	    ((elt.getAttributeNS)&&
	     (elt.getAttributeNS('toclevel','http://sbooks.net')))||
	    (elt.getAttribute('toclevel'))||
	    (elt.getAttribute('data-toclevel'));
	if (attrval) {
	    if (attrval==='none') return false;
	    else return parseInt(attrval);}
	if (elt.className) {
	    var cname=elt.className;
	    var tocloc=cname.search(/sbook\dhead/);
	    if (tocloc>=0) return parseInt(cname.slice(5,6));}
	if (elt.tagName.search(/H\d/)==0)
	    return parseInt(elt.tagName.slice(1,2));
	else return false;}
    Codex.getTOCLevel=getLevel;
    


})();

/* Adding qricons */

/*
  function sbookAddQRIcons(){
  var i=0;
  while (i<Codex.heads.length) {
  var head=Codex.heads[i++];
  var id=head.id;
  var title=(head.sbookinfo)&&sbook_get_titlepath(head.sbookinfo);
  var qrhref="https://"+Codex.server+"/glosses/qricon.png?"+
  "URI="+encodeURIComponent(Codex.docuri||Codex.refuri)+
  ((id)?("&FRAG="+head.id):"")+
  ((title) ? ("&TITLE="+encodeURIComponent(title)) : "");
  var qricon=fdjtDOM.Image(qrhref,".sbookqricon");
  fdjtDOM.prepend(head,qricon);}}
*/

/* Emacs local variables
   ;;;  Local variables: ***
   ;;;  compile-command: "cd ..; make" ***
   ;;;  End: ***
*/
/* -*- Mode: Javascript; Character-encoding: utf-8; -*- */

var codex_startup_id="$Id$";
var codex_startup_version=parseInt("$Revision$".slice(10,-1));

/* Copyright (C) 2009-2011 beingmeta, inc.
   This file implements a Javascript/DHTML UI for reading
   large structured documents (sBooks).

   For more information on sbooks, visit www.sbooks.net
   For more information on knodules, visit www.knodules.net
   For more information about beingmeta, visit www.beingmeta.com

   This library uses the FDJT (www.fdjt.org) toolkit.

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

   Use and redistribution (especially embedding in other
   CC licensed content) is permitted under the terms of the
   Creative Commons "Attribution-NonCommercial" license:

   http://creativecommons.org/licenses/by-nc/3.0/ 

   Other uses may be allowed based on prior agreement with
   beingmeta, inc.  Inquiries can be addressed to:

   licensing@beingmeta.com

   Enjoy!

*/

/* Newer startup model:
   gotInfo(info)
   info has { user: {}, outlets: [], glosses: [] }
   if _sbook_loadinfo, do gotInfo(_sbook_loadinfo);
   else do a JSONP call.
*/

var _sbook_autoindex=
    ((typeof _sbook_autoindex === 'undefined')?(false):(_sbook_autoindex));

Codex.Startup=
    (function(){

	var sbook_faketouch=false;
	var sbook_showconsole=true;

	var sbook_heading_qricons=false;

	var https_graphics=
	    "https://beingmeta.s3.amazonaws.com/static/graphics/";

	function sbicon(name,suffix) {return Codex.graphics+name+(suffix||"");}
	function cxicon(name,suffix) {
	    return Codex.graphics+"codex/"+name+(suffix||"");}

	var getLocal=fdjtState.getLocal;
	var setLocal=fdjtState.setLocal;

	/* Initialization */
	
	var _sbook_setup_start=false;
	
	var TOA=fdjtDOM.Array;

	function startupLog(){
	    var args=TOA(arguments);
	    var div=fdjtDOM("div#CODEXSTARTUPMSG",fdjtString.apply(null,args));
	    fdjtLog.apply(null,arguments);
	    fdjtDOM.replace("CODEXSTARTUPMSG",div);}

	function startupMessage(){
	    var args=TOA(arguments);
	    var div=fdjtDOM("div#CODEXSTARTUPMSG",fdjtString.apply(null,args));
	    if ((Codex.Trace.startup)&&
		(typeof Codex.Trace.startup === "number")&&
		(Codex.Trace.startup>1))
		fdjtLog.apply(null,arguments);
	    fdjtDOM.replace("CODEXSTARTUPMSG",div);}
	Codex.startupMessage=startupMessage;

	/* Configuration information */

	var config_handlers={};
	var default_config=
	    {pageview: true,
	     bodysize: 'normal',bodyfamily: 'serif',
	     uisize: 'normal',showconsole: false,
	     animatepages: true,animatehud: true,
	     hidesplash: false};
	var current_config={};

	var setCheckSpan=fdjtUI.CheckSpan.set;

	function addConfig(name,handler){
	    if (Codex.Trace.config)
		fdjtLog("Adding config handler for %s: %s",name,handler);
	    config_handlers[name]=handler;}
	Codex.addConfig=addConfig;

	function getConfig(name){
	    if (!(name)) return current_config;
	    else return current_config[name];}
	Codex.getConfig=getConfig;

	function setConfig(name,value){
	    if (arguments.length===1) {
		var config=name;
		Codex.postconfig=[];
		if (Codex.Trace.config) fdjtLog("batch setConfig: %s",config);
		for (var setting in config) {
		    if (config.hasOwnProperty(setting))
			setConfig(setting,config[setting]);}
		var dopost=Codex.postconfig;
		Codex.postconfig=false;
		if ((Codex.Trace.config)&&(!((dopost)||(dopost.length===0))))
		    fdjtLog("batch setConfig, no post processing",config);
		var i=0; var lim=dopost.length;
		while (i<lim) {
		    if (Codex.Trace.config)
			fdjtLog("batch setConfig, post processing %s",dopost[i]);
		    dopost[i++]();}
		return;}
	    if (current_config[name]===value) return;
	    if (Codex.Trace.config) fdjtLog("setConfig %o=%o",name,value);
	    var input_name="CODEX"+(name.toUpperCase());
	    var inputs=document.getElementsByName(input_name);
	    var i=0; var lim=inputs.length;
	    while (i<lim) {
		var input=inputs[i++];
		if (input.tagName!=='INPUT') continue;
		if (input.type==='checkbox') {
		    if (value) setCheckSpan(input,true);
		    else setCheckSpan(input,false);}
		else if (input.type==='radio') {
		    if (value===input.value) setCheckSpan(input,true);
		    else setCheckSpan(input,false);}
		else input.value=value;}
	    if (config_handlers[name]) {
		if (Codex.Trace.config) fdjtLog("setConfig (handler=%s) %o=%o",
						config_handlers[name],name,value);
		config_handlers[name](name,value);}
	    current_config[name]=value;}
	Codex.setConfig=setConfig;

	function saveConfig(config){
	    if (Codex.Trace.config) {
		fdjtLog("saveConfig %o",config);
		fdjtLog("current_config=%o",current_config);}
	    if (!(config)) config=current_config;
	    // Save automatically applies (seems only fair)
	    else setConfig(config);
	    var saved={};
	    for (var setting in config) {
		if ((!(default_config.hasOwnProperty(setting)))||
		    (config[setting]!==default_config[setting])) {
		    saved[setting]=config[setting];}}
	    if (Codex.Trace.config) fdjtLog("Saving config %o",saved);
	    setLocal('codex.config',JSON.stringify(saved));}
	Codex.saveConfig=saveConfig;

	function initConfig(){
	    var config=getLocal('codex.config',true);
	    Codex.postconfig=[];
	    if (Codex.Trace.config) fdjtLog("initConfig (saved) %o",config);
	    if (config) {
		for (var setting in config) {
		    if (config.hasOwnProperty(setting)) 
			setConfig(setting,config[setting]);}}
	    else config={};
	    if (Codex.Trace.config) fdjtLog("initConfig (default) %o",default_config);
	    for (var setting in default_config) {
		if (!(config[setting]))
		    if (default_config.hasOwnProperty(setting))
			setConfig(setting,default_config[setting]);}
	    var dopost=Codex.postconfig;
	    Codex.postconfig=false;
	    var i=0; var lim=dopost.length;
	    while (i<lim) dopost[i++]();
	    
	    var deviceid=current_config.deviceid;
	    var devicename=current_config.devicename;
	    if (!(deviceid)) {
		deviceid=fdjtState.getUUID();
		setConfig("deviceid",deviceid);}
	    Codex.deviceId=deviceid;
	    if (!(devicename)) {
		var vi=fdjtState.versionInfo(); var now=new Date();
		devicename=vi.browser+"/"+vi.platform+"/0"+
		    (now.getFullYear())+"/"+
		    ((now.getMonth())+1)+"/"+
		    (now.getDate())+"-"+(Math.floor(Math.random()*1000000));
		setConfig('devicename',devicename);}
	    Codex.deviceName=devicename;

	    saveConfig();}

	var getParent=fdjtDOM.getParent;
	var getChild=fdjtDOM.getChild;

	function updateConfig(name,id){
	    var elt=((typeof id === 'string')&&(document.getElementById(id)))||
		((id.nodeType)&&(getParent(id,'input')))||
		((id.nodeType)&&(getChild(id,'input')))||
		((id.nodeType)&&(getChild(id,'textarea')))||
		((id.nodeType)&&(getChild(id,'select')))||
		(id);
	    if (Codex.Trace.config) fdjtLog("Update config %s",name);
	    if ((elt.type=='radio')||(elt.type=='checkbox'))
		setConfig(name,elt.checked||false);
	    else setConfig(name,elt.value);}
	Codex.updateConfig=updateConfig;

	Codex.addConfig("hidesplash",function(name,value){
	    Codex.hidesplash=value;});
	Codex.addConfig("devicename",function(name,value){
	    Codex.deviceName=value;});
	Codex.addConfig("deviceid",function(name,value){
	    Codex.deviceId=value;});

	function Startup(force){
	    if (Codex._setup) return;
	    if ((!force)&&(fdjtState.getQuery("nosbooks"))) return; 
	    fdjtLog.console="CODEXCONSOLELOG";
	    fdjtLog.consoletoo=true;
	    fdjtLog("This is Codex version %s, built at %s on %s",
		    Codex.version,sbooks_buildtime,sbooks_buildhost);
	    if (navigator.appVersion)
		fdjtLog("App version: %s",navigator.appVersion);
	    if (!(Codex._setup_start)) Codex._setup_start=new Date();
	    // Get various settings
	    readSettings();
	    // Execute fdjt initializations
	    fdjtDOM.init();
	    // Declare this
	    fdjtDOM.addClass(document.body,"codexstartup");
	    var metadata=false;
	    var helphud=false;
	    // Initialize the databases
	    Codex.initDB();
	    // Modifies the DOM in various ways
	    initBody();
	    // This initializes the book tools (the HUD/Heads Up Display)
	    Codex.initHUD();
	    // Get any local configuration information
	    initConfig();
	    // Setup the UI components
	    Codex.setupGestures();
	    // Init user based on locally stored user information
	    if ((!(Codex.nologin))&&(getLocal("sync("+Codex.refuri+")")))
		initUserOffline();
	    // Start JSONP call to get initial or updated glosses, etc
	    var notloading=false;
	    if (Codex.nologin) {}
	    else if (window.navigator.onLine) {
		var script=fdjtDOM("SCRIPT#LOADSBOOKINFO");
		script.language="javascript";
		var uri="https://"+Codex.server+"/v1/loadinfo.js?"+
		    "REFURI="+encodeURIComponent(Codex.refuri)+"&"+
		    "CALLBACK=Codex.loadInfo";
		if (Codex.sync) uri=uri+"&SYNC="+Codex.sync;
		if (Codex.user) uri=uri+"&SYNCUSER="+
		    encodeURIComponent(Codex.user._id)
		// This is where we add a SYNC arg
		script.src=uri;
		document.body.appendChild(script);}
	    else notloading=true;
	    fdjtTime.timeslice
	    ([// Setup sbook tables, databases, etc
		appSplash,
		// Scan the DOM for metadata.  THis is surprisingly fast,
		//  so we don't currently try to timeslice it, though we could
		function(){
		    // This scans the DOM.  It would probably be a good
		    //  idea to do this asynchronously
		    metadata=new CodexDOMScan(Codex.root);
		    fdjtDOM.addClass(metadata._heads,"avoidbreakafter");
		    Codex.docinfo=Codex.DocInfo.map=metadata;
		    Codex.ends_at=Codex.docinfo[Codex.root.id].ends_at;
		    if (Codex.afterscan) {
			var donefn=Codex.afterscan;
			delete Codex.afterscan;
			donefn();}},
		// Now you're ready to lay out the book, which is
		//  timesliced and runs on its own.  We wait to do
		//  this until we've scanned the DOM because we may
		//  use results of DOM scanning in layout.
		function(){if (Codex.paginate) Codex.Paginate("initial");},
		// Build the display TOC, both the dynamic (top of
		// display) and the static (inside the flyleaf)
		function(){
		    startupLog("Building table of contents based on %d heads",
			       Codex.docinfo._headcount);
		    Codex.setupTOC(metadata[Codex.root.id]);},
		// Read knowledge bases (knodules) used by the book
		((Knodule)&&(Knodule.HTML)&&
		 (Knodule.HTML.Setup)&&(Codex.knodule)&&
		 (function(){
		     startupLog("Processing knodule %s",Codex.knodule.name);
		     Knodule.HTML.Setup(Codex.knodule);})),
		// Process any preloaded user/gloss information
		((_sbook_loadinfo)&&(function(){loadInfo(_sbook_loadinfo);})),
		// Process locally stored glosses if not loading
		((notloading)&&(getLocal("sync("+Codex.refuri+")"))&&
		 initGlossesOffline),
		// Index tags for search
		function(){
		    startupLog("Indexing tags for search");
		    applyInlineTags();
		    startupLog("Indexing tag attributes from the source");
		    indexContentTags(metadata);
		    startupLog("Indexing inline (Technorati-style) tags");
		    indexInlineTags(Codex.knodule);
		    // This table is generally loaded as part of the book 
		    if (_sbook_autoindex) {
			startupLog("Indexing automatic tags");
			Codex.useAutoIndex(_sbook_autoindex,Codex.knodule);
			_sbook_autoindex=false;}},
		function(){
		    startupLog("Setting up tag clouds"); initClouds();},
		// Figure out which mode to start up in, based on
		// query args to the book.
		function(){
		    if ((fdjtState.getQuery("join"))||
			(fdjtState.getQuery("action"))||
			(fdjtState.getQuery("invitation"))) {
			CodexMode("sbookapp");}
		    else if (fdjtState.getQuery("startmode")) 
			CodexMode(fdjtState.getQuery("startmode"));
		    if ((!(Codex.paginate))||(Codex.paginated))
			startupDone();
		    else Codex.pagewait=startupDone;}],
	     100,25);}
	Codex.Startup=Startup;
	
	function appSplash(){
	    // Take any message passed along as a query string
	    //  and put it in the top of the help window, then
	    //  display the help window
	    if (fdjtState.getQuery("congratulations"))
		fdjtDOM(fdjtID("CODEXINTRO"),
			fdjtDOM("strong","Congratulations, "),
			fdjtState.getQuery("congratulations"));
	    else if (fdjtState.getQuery("sorry"))
		fdjtDOM(fdjtID("CODEXINTRO"),
			fdjtDOM("strong","Sorry, "),
			fdjtState.getQuery("sorry"));
	    else if (fdjtState.getQuery("weird")) 
		fdjtDOM(fdjtID("CODEXINTRO"),
			fdjtDOM("strong","Weird, "),
			fdjtState.getQuery("weird"));
	    if (fdjtState.getQuery("ACTION"))
		CodexMode("sbookapp");
	    else CodexMode("help");
	    // Hide the splash page, if any
	    if (fdjtID("CODEXSPLASH"))
		fdjtID("CODEXSPLASH").style.display='none';
	    window.focus();}
     
	function startupDone(){
	    initLocation();
	    if (fdjtID("CODEXREADYSPLASH"))
		fdjtID("CODEXREADYSPLASH").style.display='none';
	    Codex.displaySync();
	    setInterval(Codex.serverSync,60000);
	    fdjtDOM.dropClass(document.body,"codexstartup");
	    if ((Codex.mode==='help')&&(Codex.hidesplash)) {
		CodexMode(false);}
	    _sbook_setup=Codex._setup=new Date();}

	/* Application settings */

	var optrules=
	    {"paginate":["scrolling"],
	     "scrolling":["paginate"],
	     "touch":["mouse","kbd"],
	     "mouse":["touch","kbd"],
	     "kbd":["touch","mouse"]};

	function setopt(opt,flag){
	    if (typeof flag === 'undefined') flag=true;
	    if ((flag)&&(sbook[opt])) return;
	    else if ((!(flag))&&(!(sbook[opt]))) return;
	    var unset=optrules[opt];
	    sbook[opt]=true;
	    if (unset) {
		var i=0; var lim=unset.length;
		sbook[unset[i++]]=false;}}

	function workOffline(refuri){
	    var value=fdjtState.getQuery("offline")||
		getLocal("offline("+refuri+")")||
		getLocal("mycopyid("+refuri+")")||
		getLocal("sbooks.offline")||
		((fdjtDOM.getMeta("sbook.mycopyid")))||
		((fdjtDOM.getMeta("sbooks.mycopyid")))||
		((fdjtDOM.getMeta("MYCOPYID")))||
		(fdjtDOM.getMeta("sbook.offline"));
	    if ((!(value))||(value==="no")||(value==="off")||(value==="never"))
		return false;
	    else if ((value==="ask")&&(window.confirm))
		return window.confirm("Read offline?");
	    else return true;}
	
	var glossref_classes=false;

	function readSettings(){
	    if (typeof _sbook_loadinfo === "undefined") _sbook_loadinfo=false;
	    if (typeof _sbook_glosses === "undefined") _sbook_glosses=false;
	    // Basic stuff
	    var useragent=navigator.userAgent;
	    var refuri=_getsbookrefuri();
	    document.body.refuri=Codex.refuri=refuri;
	    Codex.docuri=_getsbookdocuri();
	    Codex.devinfo=fdjtState.versionInfo();
	    
	    var refuris=getLocal("sbooks.refuris",true)||[];
	    var offline=workOffline(refuri);
	    Codex.offline=((offline)?(true):(false));
	    
	    // Get the settings for scanning the document structure
	    getScanSettings();

	    // Where to get your images from, especially to keep referenes
	    //  inside https
	    if ((Codex.graphics==="http://static.beingmeta.com/graphics/")&&
		(window.location.protocol==='https:'))
		Codex.graphics=https_graphics;
	    
	    // Whether to suppress login, etc
	    if ((getLocal("sbooks.nologin"))||(fdjtState.getQuery("nologin")))
		Codex.nologin=true;
	    if ((getLocal("sbooks.nopage"))||(fdjtState.getQuery("nopage"))) {
		default_config.pageview=false;
		Codex.paginate=false;}
	    Codex.max_excerpt=fdjtDOM.getMeta("sbook.maxexcerpt")||
		(Codex.max_excerpt);
	    Codex.min_excerpt=fdjtDOM.getMeta("sbook.minexcerpt")||
		(Codex.min_excerpt);
	    var sbooksrv=fdjtDOM.getMeta("sbook.server")||
		fdjtDOM.getMeta("SBOOKSERVER");
	    if (sbooksrv) Codex.server=sbooksrv;
	    else if (fdjtState.getCookie["SBOOKSERVER"])
		Codex.server=fdjtState.getCookie["SBOOKSERVER"];
	    else Codex.server=lookupServer(document.domain);
	    if (!(Codex.server)) Codex.server=Codex.default_server;
	    sbook_ajax_uri=fdjtDOM.getMeta("sbook.ajax",true);

	    refuris.push(refuri);

	    if ((!(Codex.nologin))&&(Codex.offline)) {
		Codex.mycopyid=fdjtDOM.getMeta("sbook.mycopyid")||
		    ((offline)&&(getLocal("mycopy("+refuri+")")))||
		    false;
		setLocal("sbooks.refuris",refuris,true);}	    

	    var isIphone = (/iphone/gi).test(navigator.appVersion);
	    var isTouchPad = (/Touchpad/gi).test(navigator.appVersion);
	    var isIpad = (/ipad/gi).test(navigator.appVersion);
	    var isAndroid = (/android/gi).test(navigator.appVersion);
	    var isWebKit = navigator.appVersion.search("WebKit")>=0;
	    var isWebTouch = isIphone || isIpad || isAndroid || isTouchPad;

	    if ((typeof Codex.colbreak === 'undefined')&&
		((Codex.devinfo.Chrome)||
		 ((Codex.devinfo.AppleWebKit)&&
		  (Codex.devinfo.Mobile)&&
		  (Codex.devinfo.AppleWebKit>532))||
		 ((Codex.devinfo.AppleWebKit)&&
		  (Codex.devinfo.AppleWebKit>533)))) {
		Codex.colbreak=true;
		Codex.talldom=true;}
	    if (isWebTouch) {
		fdjtDOM.addClass(document.body,"sbooktouchui");
		viewportSetup();
		Codex.ui="webtouch"; Codex.touch=true;}
	    if ((useragent.search("Safari/")>0)&&
		(useragent.search("Mobile/")>0)) { 
		hide_mobile_safari_address_bar();
		Codex.nativescroll=false;
		Codex.scrolldivs=false;
		Codex.updatehash=false;
		// Have fdjtLog do it's own format conversion for the log
		fdjtLog.doformat=true;}
	    else if (sbook_faketouch) {
		fdjtDOM.addClass(document.body,"sbooktouchui");
		viewportSetup();
		Codex.ui="faketouch"}
	    else {
		fdjtDOM.addClass(document.body,"sbookmouseui");
		// fdjtDOM.addClass(document.body,"sbooktouchui");
		// fdjtDOM.addClass(document.body,"codexscalebody");
		Codex.ui="mouse";}
	    
	    Codex.allglosses=[];
	    Codex.allsources=[];
	    Codex.etc=[];}

	function initUserOffline(){
	    var refuri=Codex.refuri;
	    var sync=getLocal("sync("+refuri+")",true);
	    if (!(sync)) return;
	    var user=getLocal("user("+refuri+")");
	    var nodeid=getLocal("nodeid("+refuri+")");
	    if (!(user)) return;
	    var userinfo=getLocal(user,true);
	    var outlets=Codex.outlets=getLocal("outlets("+refuri+")",true)||[];
	    Codex.allsources=getLocal("sources("+refuri+")",true)||[];
	    Codex.sourcekb.Import(Codex.allsources);
	    if (userinfo) setUser(userinfo,outlets,sync);
	    if (nodeid) setNodeID(nodeid);
	    Codex.sync=sync;}

	function initGlossesOffline(){
	    var refuri=Codex.refuri;
	    var sync=getLocal("sync("+refuri+")",true);
	    if (!(sync)) return;
	    var localglosses=getLocal("glosses("+refuri+")",true)||[];
	    var queuedglosses=getLocal("queued("+refuri+")",true)||[];
	    var allglosses=Codex.allglosses||[];
	    localglosses=localglosses.concat(queuedglosses);
	    Codex.localglosses=localglosses;
	    Codex.allglosses=allglosses.concat(localglosses);
	    Codex.etc=getLocal("etc("+refuri+")",true)||[];
	    var i=0; var lim=localglosses.length;
	    var glossdb=Codex.glosses;
	    while (i<lim) {
		var glossid=localglosses[i++];
		var gloss=glossdb.ref(glossid);
		gloss.load();
		if (Codex.Trace.offline>1)
		    fdjtLog("Restored %o: %j",glossid,gloss);}
	    var etc=Codex.etc;
	    var i=0; var lim=etc.length;
	    while (i<lim) Codex.sourcekb.ref(etc[i++]);}

	/* Viewport setup */

	var viewport_spec="width=device-width,initial-scale=1.0";
	function viewportSetup(){
	    var head=fdjtDOM.getHEAD();
	    var viewport=fdjtDOM.getMeta("viewport",false,false,true);
	    if (!(viewport)) {
		viewport=document.createElement("META");
		viewport.setAttribute("name","viewport");
		head.appendChild(viewport);}
	    viewport.setAttribute("content",viewport_spec);
	    var isapp=fdjtDOM.getMeta
	    ("apple-mobile-web-app-capable",false,false,true);
	    if (!(isapp)) {
		isapp=document.createElement("META");
		isapp.setAttribute("name","apple-mobile-web-app-capable");
		head.appendChild(isapp);}}

	function hide_mobile_safari_address_bar(){
	    window.scrollTo(0,1);
	    setTimeout(function(){window.scrollTo(0,0);},0);}

	/* Getting settings */

	function _getsbookrefuri(){
	    var refuri=fdjtDOM.getLink("sbook.refuri",false,false)||
		fdjtDOM.getLink("refuri",false,false)||
		fdjtDOM.getMeta("sbook.refuri",false,false)||
		fdjtDOM.getMeta("refuri",false,false)||
		fdjtDOM.getLink("canonical",false,true);
	    if (refuri) return decodeURI(refuri);
	    else {
		var locref=document.location.href;
		var qstart=locref.indexOf('?');
		if (qstart>=0) locref=locref.slice(0,qstart);
		var hstart=locref.indexOf('#');
		if (hstart>=0) locref=locref.slice(0,hstart);
		return decodeURI(locref);}}
	function _getsbookdocuri(){
	    return fdjtDOM.getLink("sbook.docuri",false)||
		fdjtDOM.getLink("docuri",false)||
		fdjtDOM.getMeta("sbook.docuri",false)||
		fdjtDOM.getMeta("docuri",false)||
		fdjtDOM.getLink("canonical",false)||
		location.href;}

	function lookupServer(string){
	    var sbook_servers=Codex.servers;
	    var i=0;
	    while (i<sbook_servers.length) 
		if (sbook_servers[i][0]===string)
		    return sbook_servers[i][1];
	    else if (string.search(sbook_servers[i][0])>=0)
		return sbook_servers[i][1];
	    else if ((sbook_servers[i][0].call) &&
		     (sbook_servers[i][0].call(string)))
		return sbook_servers[i][1];
	    else i++;
	    return false;}

	function hasTOCLevel(elt){
	    if ((elt.toclevel)||
		((elt.getAttributeNS)&&
		 (elt.getAttributeNS('toclevel','http://sbooks.net/')))||
		(elt.getAttribute('toclevel'))||
		(elt.getAttribute('data-toclevel'))||
		((elt.className)&&
		 ((elt.className.search(/\bsbook\dhead\b/)>=0)||
		  (elt.className.search(/\bsbooknotoc\b/)>=0)||
		  (elt.className.search(/\bsbookignore\b/)>=0))))
		return true;
	    else return false;}
	Codex.hasTOCLevel=hasTOCLevel;

	var headlevels=["not","A","B","C","D","E","F","G","H","I","J","K","L"];

	function getScanSettings(){
	    if (!(Codex.root))
		if (fdjtDOM.getMeta("sbook.root"))
		    Codex.root=fdjtID(fdjtDOM.getMeta("sbook.root"));
	    else Codex.root=fdjtID("SBOOKCONTENT")||document.body;
	    if (!(Codex.start))
		if (fdjtDOM.getMeta("sbook.start"))
		    Codex.start=fdjtID(fdjtDOM.getMeta("sbook.start"));
	    else if (fdjtID("SBOOKSTART"))
		Codex.start=fdjtID("SBOOKSTART");
	    else {
		var titlepage=fdjtID("SBOOKTITLE")||fdjtID("TITLEPAGE");
		while (titlepage)
		    if (fdjtDOM.nextElt(titlepage)) {
			Codex.start=fdjtDOM.nextElt(titlepage); break;}
		else titlepage=titlepage.parentNode;}
	    var i=0; while (i<9) {
		var rules=fdjtDOM.getMeta("sbook.head"+i,true).
		    concat(fdjtDOM.getMeta("sbook"+i+"head",true)).
		    concat(fdjtDOM.getMeta("sbook"+headlevels[i]+"head",true));
		if ((rules)&&(rules.length)) {
		    var j=0; var lim=rules.length;
		    var elements=fdjtDOM.getChildren(document.body,rules[j++]);
		    var k=0; var n=elements.length;
		    while (k<n) {
			var elt=elements[k++];
			if (!(hasTOCLevel(elt))) elt.toclevel=i;}}
		i++;}
	    if (fdjtDOM.getMeta("sbookignore")) 
		Codex.ignore=new fdjtDOM.Selector(fdjtDOM.getMeta("sbookignore"));
	    if (fdjtDOM.getMeta("sbooknotoc")) 
		Codex.notoc=new fdjtDOM.Selector(fdjtDOM.getMeta("sbooknotoc"));
	    if (fdjtDOM.getMeta("sbookterminal"))
		Codex.terminal_rules=
		new fdjtDOM.Selector(fdjtDOM.getMeta("sbookterminal"));
	    if (fdjtDOM.getMeta("sbookid")) 
		sbook_idify=new fdjtDOM.Selector(fdjtDOM.getMeta("sbookid"));
	    if ((fdjtDOM.getMeta("sbookfocus"))) 
		Codex.focus=new fdjtDOM.Selector(fdjtDOM.getMeta("sbookfocus"));
	    if (fdjtDOM.getMeta("sbooknofocus"))
		Codex.nofocus=new fdjtDOM.Selector(fdjtDOM.getMeta("sbooknofocus"));}

	function applyMetaClass(name){
	    var meta=fdjtDOM.getMeta(name,true);
	    var i=0; var lim=meta.length;
	    while (i<lim) fdjtDOM.addClass(fdjtDOM.$(meta[i++]),name);}

	var note_count=1;
	function initBody(){
	    var body=document.body;
	    var content=fdjtDOM("div#CODEXCONTENT");
	    var nodes=fdjtDOM.toArray(body.childNodes);
	    var style=fdjtDOM("STYLE");
	    fdjtDOM(document.head,style);
	    Codex.stylesheet=style.sheet;
	    var i=0; var lim=nodes.length;
	    while (i<lim) {
		var node=nodes[i++];
		if (node.nodeType===1) {
		    if ((node.tagName!=='LINK')&&(node.tagName!=='META')&&
			(node.tagName!=='SCRIPT'))
			content.appendChild(node);}
		else content.appendChild(node);}
	    Codex.content=content;
	    Codex.coverpage=fdjtID("SBOOKCOVERPAGE");
	    Codex.titlepage=fdjtID("SBOOKTITLEPAGE");
	    fdjtDOM.addClass(document.body,"codexscalebody");
	    var allnotes=fdjtID("SBOOKNOTES");
	    var allasides=fdjtID("SBOOKASIDES");
	    var alldetails=fdjtID("SBOOKDETAILS");
	    if (!(alldetails)) {
		var alldetails=fdjtDOM("div#SBOOKDETAILS");
		fdjtDOM(content,alldetails);}
	    if (!(allasides)) {
		var allasides=fdjtDOM("div#SBOOKASIDES");
		fdjtDOM(content,allasides);}
	    if (!(allnotes)) {
		var allnotes=fdjtDOM("div.sbookbackmatter#SBOOKNOTES");
		fdjtDOM(content,allnotes);}
	    var page=Codex.page=fdjtDOM(
		"div#CODEXPAGE",
		fdjtDOM("div#CODEXPAGINATING","Laid out ",
			fdjtDOM("span#CODEXPAGEPROGRESS",""),
			" pages"),
		Codex.pages=fdjtDOM("div#CODEXPAGES"));
	    fdjtDOM(body,content,page);
	    fdjtDOM.addClass(body,"sbook");
	    var page_width=fdjtDOM.getGeometry(page).width;
	    var view_width=fdjtDOM.viewWidth();
	    var page_margin=(view_width-page_width)/2;
	    if (page_margin>=50) {
		page.style.left=page_margin+'px';
		page.style.right=page_margin+'px';}
	    applyMetaClass("sbookdetails");
	    applyMetaClass("sbooknoteref");
	    applyMetaClass("sbookbibref");
	    applyMetaClass("sbookxnote");
	    applyMetaClass("sbookaside");
	    applyMetaClass("sbookbackmatter");
	    var sbookxnotes=fdjtDOM.$("sbookxnote");
	    // Add refs for all of the xnotes
	    var i=0; var lim=sbookxnotes.length;
	    while (i<lim) {
		var note=sbookxnotes[i++];
		var anchor=fdjtDOM("A.sbooknoteref","\u2193");
		var count=note_count++;
		anchor.id="SBOOKNOTEREF"+count;
		if (!(note.id)) note.id="SBOOKNOTE"+count;
		anchor.href="#"+note.id;
		fdjtDOM.insertBefore(note,anchor);}
	    // Move all the notes to the end
	    var noterefs=fdjtDOM.$(".sbooknoteref,.sbookbibref");
	    var i=0; var lim=noterefs.length;
	    while (i<lim) {
		var noteref=noterefs[i++];
		var idcontext=Codex.getTarget(noteref.parentNode);
		if ((noteref.href)&&(noteref.href[0]==='#')) {
		    var noteid=noteref.href.slice(1);
		    var notenode=fdjtID(noteid);
		    if (!(notenode)) continue;
		    if ((noteref.id)||(idcontext)) {
			var backanchor=fdjtDOM("A.sbooknotebackref","\u2191");
			backanchor.href="#"+noteref.id||(idcontext.id);
			fdjtDOM.prepend(notenode,backanchor);}
		    if ((idcontext)&&(fdjtDOM.hasClass(noteref,"sbooknoteref")))
			notenode.codextocloc=idcontext.id;
		    if ((fdjtDOM.hasClass(noteref,"sbooknoteref"))&&
			(!(fdjtDOM.hasParent(notenode,".sbookbackmatter"))))
			fdjtDOM.append(allnotes,notenode);}}
	    // Move all the details to the end
	    var details=fdjtDOM.$("detail,.sbookdetail");
	    var i=0; var lim=details.length;
	    while (i<lim) {
		var detail=details[i++];
		var head=fdjtDOM.getChild(detail,"summary,.sbooksummary");
		var detailhead=
		    ((head)?(fdjtDOM.clone(head)):
		     fdjtDIV("div.sbookdetailstart",
			     (fdjtString.truncate(fdjtDOM.textify(detail),42))));
		var anchor=fdjtDOM("A.sbookdetailref",detailhead);
		var count=detail_count++;
		if (!(detail.id)) detail.id="SBOOKDETAIL"+count;
		anchor.href="#"+detail.id; anchor.id="SBOOKDETAILREF"+count;
		fdjtDOM.replace(detail,anchor);
		detail.codextocloc=anchor.id;
		fdjtDOM.append(alldetails,detail);}
	    // Move all the asides to the end
	    var asides=fdjtDOM.$("aside,.sbookaside");
	    var i=0; var lim=asides.length;
	    while (i<lim) {
		var aside=asides[i++];
		var head=fdjtDOM.getChild(aside,".sbookasidehead")||
		    fdjtDOM.getChild(aside,"HEADER")||
		    fdjtDOM.getChild(aside,"H1")||
		    fdjtDOM.getChild(aside,"H2")||
		    fdjtDOM.getChild(aside,"H3")||
		    fdjtDOM.getChild(aside,"H4")||
		    fdjtDOM.getChild(aside,"H5")||
		    fdjtDOM.getChild(aside,"H6");
		var asidehead=((head)?(fdjtDOM.clone(head)):
			       fdjtDIV("div.sbookasidestart",
				       (fdjtString.truncate(fdjtDOM.textify(aside),42))));
		var anchor=fdjtDOM("A.sbookasideref",asidehead);
		var count=aside_count++;
		if (!(aside.id)) aside.id="SBOOKASIDE"+count;
		anchor.href="#"+aside.id; anchor.id="SBOOKASIDEREF"+count;
		fdjtDOM.insertBefore(aside,anchor);
		aside.codextocloc=anchor.id;
		fdjtDOM.append(allasides,aside);}
	    var humane=fdjtDOM.$(".humane");
	    if (humane) {
		var i=0; var lim=humane.length;
		while (i<lim) humane[i++].codexui=true;}
	    // Initialize the margins
	    initMargins();
	    if (Codex.Trace.startup>1)
		fdjtLog("Initialized body");}
	
	/* Margin creation */

	function initMargins(){
	    var topleading=fdjtDOM("div#SBOOKTOPLEADING.leading.top"," ");
	    var bottomleading=fdjtDOM("div#SBOOKBOTTOMLEADING.leading.bottom"," ");
	    topleading.codexui=true; bottomleading.codexui=true;
	    
	    var pagehead=fdjtDOM("div.codexmargin#CODEXPAGEHEAD"," ");
	    var pageright=fdjtDOM("div#CODEXPAGERIGHT");
	    var pageleft=fdjtDOM("div#CODEXPAGELEFT");
	    
	    var pageinfo=
		fdjtDOM("div#CODEXPAGEINFO",
			fdjtDOM("div.progressbar#CODEXPROGRESSBAR",""),
			fdjtDOM("div#CODEXPAGENO",
				fdjtDOM("span#CODEXPAGENOTEXT","p/n")));
	    var pagefoot=fdjtDOM("div.codexmargin#CODEXPAGEFOOT",pageinfo," ");
	    pagehead.codexui=true; pagefoot.codexui=true;
	    sbookPageHead=pagehead; sbookPageFoot=pagefoot;

	    fdjtDOM.prepend(document.body,pagehead,pagefoot,pageleft,pageright);

	    for (var pagelt in [pagehead,pageright,pageleft,pagefoot,pageinfo]) {
		fdjtDOM.addListeners(
		    pageinfo,Codex.UI.handlers[Codex.ui]["#"+pagelt.id]);}
		
	    window.scrollTo(0,0);
	    
	    // The better way to do this might be to change the stylesheet,
	    //  but fdjtDOM doesn't currently handle that 
	    var bgcolor=getBGColor(document.body)||"white";
	    if (bgcolor==='transparent')
		bgcolor=fdjtDOM.getStyle(document.body).backgroundColor;
	    if ((bgcolor)&&(bgcolor.search("rgba")>=0)) {
		if (bgcolor.search(/,\s*0\s*\)/)>0) bgcolor='white';
		else {
		    bgcolor=bgcolor.replace("rgba","rgb");
		    bgcolor=bgcolor.replace(/,\s*((\d+)|(\d+.\d+))\s*\)/,")");}}
	    else if (bgcolor==="transparent") bgcolor="white";
	    pagehead.style.backgroundColor=bgcolor;
	    pagefoot.style.backgroundColor=bgcolor;
	    fdjtDOM.addListener(false,"resize",function(evt){
		if (Codex.paginate) CodexLayout.onresize(evt||event);});}
	
	function getBGColor(arg){
	    var color=fdjtDOM.getStyle(arg).backgroundColor;
	    if (!(color)) return false;
	    else if (color==="transparent") return false;
	    else if (color.search(/rgba/)>=0) return false;
	    else return color;}

	/* Loading meta info (user, glosses, etc) */

	function loadInfo(info) {
	    if (!(Codex.docinfo)) /* Scan not done */
		Codex.scandone=function(){loadInfo(info);};
	    else if (info.loaded) return;
	    var refuri=Codex.refuri;
	    if ((Codex.offline)&&
		(info)&&(info.userinfo)&&(Codex.user)&&
		(info.userinfo._id!==Codex.user._id)) {
		clearOffline(refuri);}
	    var persist=((Codex.offline)&&(navigator.onLine));
	    info.loaded=fdjtTime();
	    if (info.userinfo)
		setUser(info.userinfo,info.outlets,info.sync);
	    if (info.nodeid) setNodeID(info.nodeid);
	    if ((!(Codex.localglosses))&&
		((getLocal("sync("+refuri+")"))||
		 (getLocal("queued("+refuri+")"))))
		initGlossesOffline();
	    if (info.sources) gotInfo("sources",info.sources,persist);
	    if (info.glosses) initGlosses(info.glosses,info.etc);
	    if (info.sync) setLocal("sync("+refuri+")",info.sync);}
	Codex.loadInfo=loadInfo;

	function getUser() {
	    var refuri=Codex.refuri;
	    var loadinfo=_sbook_loadinfo||false;
	    if (Codex.Trace.startup>1)
		fdjtLog("Getting user for %o cur=%o",refuri,Codex.user);
	    if (Codex.user) return Codex.user;
	    else if (Codex.nologin) return false;
	    if ((loadinfo)&&(gotUser(loadinfo)))
		return Codex.user;
	    else if ((typeof _sbook_userinfo !== 'undefined')&&
		     gotUser(_sbook_userinfo))
		return Codex.user;
	    if (getLocal("sbooks.user")) {
		var user=getLocal("sbooks.user");
		if (Codex.Trace.startup)
		    fdjtLog("Restoring offline user info for %o reading %o",
			    user,refuri);
		var userinfo=JSON.parse(getLocal(user));
		var sources=getLocal("sources("+refuri+")",true);
		var outlets=getLocal("outlets("+refuri+")",true);
		var nodeid=getLocal("nodeid("+refuri+")");
		var sync=getLocal("sbooks.usersync",true);
		gotUser(userinfo,nodeid,sources,outlets,etcinfo,sync);
		return;}
	    else if (!(fdjtID("SBOOKGETUSERINFO"))) {
		var user_script=fdjtDOM("SCRIPT#SBOOKGETUSERINFO");
		user_script.language="javascript";
		user_script.src=
		    "https://"+Codex.server+"/v1/loadinfo.js?REFURI="+
		    encodeURIComponent(codex.refuri)+"&CALLBACK=Codex.gotInfo";
		document.body.appendChild(user_script);
		fdjtDOM.addClass(document.body,"notsbookuser");}
	    else fdjtDOM.addClass(document.body,"notsbookuser");}
	
	function setUser(userinfo,outlets,sync){
	    var persist=((Codex.offline)&&(navigator.onLine));
	    var refuri=Codex.refuri;
	    if (userinfo) {
		fdjtDOM.dropClass(document.body,"notsbookuser");
		fdjtDOM.addClass(document.body,"sbookuser");}
	    if (Codex.user)
		if (userinfo._id===Codex.user._id) {}
	    else throw { error: "Can't change user"};
	    var cursync=Codex.sync;
	    if ((cursync)&&(cursync>=sync)) {
		fdjtLog.warn(
		    "Cached user information is newer (%o) than loaded (%o)",
		    cursync,sync);
		return false;}
	    Codex.user=fdjtKB.Import(userinfo);
	    if (persist) {
		setLocal(Codex.user._id,Codex.user,true);
		setLocal("sbooks.user",Codex.user._id);
		setLocal("user("+refuri+")",Codex.user._id);}
	    gotInfo("outlets",outlets,persist);
	    if ((outlets)&&(outlets.length)) {
		Codex.outlets=outlets;
		var addgloss=fdjtID("CODEXADDGLOSSPROTOTYPE");
		var div=fdjtDOM.getChild(addgloss,".outlets");
		fdjtDOM.dropClass(div,"nocontent");
		var i=0; var ilim=outlets.length;
		while (i<ilim) {
		    var outlet=outlets[i++];
		    var checkspan=
			fdjtUI.CheckSpan(
			    "span.checkspan",
			    "SHARE",outlet._id,false,
			    outlet.nick||outlet.name);
		    if ((outlet.description)&&(outlet.nick))
			checkspan.title=outlet.name+": "+outlet.description;
		    else if (outlet.description)
			checkspan.title=outlet.description;
		    else if (outlet.nick) checkspan.title=outlet.name;
		    fdjtDOM(div,checkspan,"\n");}}
	    setupUI4User();
	    return Codex.user;}
	Codex.setUser=setUser;
	
	function setNodeID(nodeid){
	    var refuri=Codex.refuri;
	    if (!(Codex.nodeid)) {
		Codex.nodeid=nodeid;
		if ((nodeid)&&(Codex.offline))
		    setLocal("nodeid("+refuri+")",nodeid);}}
	Codex.setNodeID=setNodeID;

	function setupUI4User(){
	    if (Codex._user_setup) return;
	    if (!(Codex.user)) {
		fdjtDOM.addClass(document.body,"notsbookuser");
		return;}
	    fdjtDOM.dropClass(document.body,"notsbookuser");
	    var username=Codex.user.name;
	    if (fdjtID("SBOOKUSERNAME"))
		fdjtID("SBOOKUSERNAME").innerHTML=username;
	    var names=document.getElementsByName("CODEXUSERNAME");
	    if (names) {
		var i=0, lim=names.length;
		while (i<lim) names[i++].innerHTML=username;}
	    if (fdjtID("SBOOKMARKUSER"))
		fdjtID("SBOOKMARKUSER").value=Codex.user._id;

	    /* Initialize add gloss prototype */
	    var ss=Codex.stylesheet;
	    var form=fdjtID("CODEXADDGLOSSPROTOTYPE");
	    var getChild=fdjtDOM.getChild;
	    if (Codex.user.fbid)  {
		ss.insertRule("span.facebook_share { display: inline;}",
			      ss.cssRules.length);
		var cs=getChild(form,".checkspan.facebook_share");
		fdjtUI.CheckSpan.set(cs,true);
		var cb=getChild(cs,"input");
		cb.setAttribute("checked","checked");}
	    if (Codex.user.twitterid) {
		ss.insertRule("span.twitter_share { display: inline;}",
			     ss.cssRules.length);
		var cs=getChild(form,".checkspan.twitter_share");
		fdjtUI.CheckSpan.set(cs,true);
		var cb=getChild(cs,"input");
		cb.setAttribute("checked","checked");}
	    if (Codex.user.linkedinid) {
		ss.insertRule("span.linkedin_share { display: inline;}",
			     ss.cssRules.length);
		var cs=getChild(form,".checkspan.linkedin_share");
		fdjtUI.CheckSpan.set(cs,true);
		var cb=getChild(cs,"input");
		cb.setAttribute("checked","checked");}
	    if (Codex.user.googleid) {
		ss.insertRule("span.google_share { display: inline;}",
			     ss.cssRules.length);
		var cs=getChild(form,".checkspan.google_share");
		fdjtUI.CheckSpan.set(cs,true);
		var cb=getChild(cs,"input");
		cb.setAttribute("checked","checked");}
	    
	    var pic=
		(Codex.user.pic)||
		((Codex.user.fbid)&&
		 ("https://graph.facebook.com/"+Codex.user.fbid+
		  "/picture?type=square"));
	    if (pic) {
		if (fdjtID("SBOOKMARKIMAGE")) fdjtID("SBOOKMARKIMAGE").src=pic;
		if (fdjtID("SBOOKUSERPIC")) fdjtID("SBOOKUSERPIC").src=pic;
		var byname=document.getElementsByName("SBOOKUSERPIC");
		if (byname) {
		    var i=0; var lim=byname.length;
		    while (i<lim) byname[i++].src=pic;}}
	    if (fdjtID("SBOOKFRIENDLYOPTION"))
		if (Codex.user)
		    fdjtID("SBOOKFRIENDLYOPTION").value=Codex.user._id;
	    else fdjtID("SBOOKFRIENDLYOPTION").value="";
	    var idlinks=document.getElementsByName("IDLINK");
	    if (idlinks) {
		var i=0; var len=idlinks.length;
		while (i<len) {
		    var idlink=idlinks[i++];
		    idlink.target='_blank';
		    idlink.title='click to edit your personal information';
		    idlink.href='https://auth.sbooks.net/admin/identity';}}
	    if (Codex.user.friends) {
		var friends=Codex.user.friends;
		var i=0; var lim=friends.length;
		while (i<lim) {
		    var friend=fdjtKB.ref(friends[i++]);
		    Codex.addTag2UI(friend);}}
	    Codex._user_setup=true;}
	
	// Processes info loaded remotely
	function gotInfo(name,info,persist) {
	    var refuri=Codex.refuri;
	    if (info)
		if (info instanceof Array) {
		    var i=0; var lim=info.length; var qids=[];
		    while (i<lim) {
			if (typeof info[i] === 'string') {
			    var qid=info[i++];
			    if (Codex.offline) fdjtKB.load(qid);
			    qids.push(qid);}
			else {
			    var obj=fdjtKB.Import(info[i++]);
			    if (persist) 
				setLocal(obj._id,obj,true);
			    qids.push(obj._id);}}
		    sbook[name]=qids;
		    if (Codex.offline)
			setLocal(
			    "sbooks."+name+"("+refuri+")",qids,true);}
	    else {
		var obj=fdjtKB.Import(info);
		if (persist) 
		    setLocal(obj._id,obj,true);
		sbook[name]=obj._id;
		if (persist)
		    setLocal("sbooks."+name+"("+refuri+")",qid,true);}}

	function setupGlosses(newglosses) {
	    var allglosses=Codex.allglosses||[];
	    Codex.glosses.Import(newglosses);
	    if (newglosses.length) {
		var n=newglosses.length; var i=0; while (i<n) {
		    var gloss=newglosses[i++];
		    var id=gloss._id;
		    var tstamp=gloss.syncstamp||gloss.tstamp;
		    if (tstamp>latest) latest=tstamp;
		    allglosses.push(id);}}
	    Codex.syncstamp=latest;
	    Codex.allglosses=allglosses;
	    if (Codex.offline) 
		setLocal("glosses("+Codex.refuri+")",allglosses,true);}

	function initGlosses(glosses,etc){
	    var allglosses=Codex.allglosses;
	    if (etc) {
		startupLog("Assimilating %d new glosses/%d sources...",
			   glosses.length,etc.length);}
	    else {
		startupLog("Assimilating %d new glosses...",glosses.length);}
	    fdjtKB.Import(etc);
	    Codex.glosses.Import(glosses);
	    var i=0; var lim=glosses.length;
	    var latest=Codex.syncstamp||0;
	    while (i<lim) {
		var gloss=glosses[i++]; var id=gloss._id;
		var tstamp=gloss.syncstamp||gloss.tstamp;
		if (tstamp>latest) latest=tstamp;
		allglosses.push(id);}
	    Codex.syncstamp=latest;
	    Codex.allglosses=allglosses;
	    if (Codex.offline) 
		setLocal("glosses("+Codex.refuri+")",allglosses,true);}
	Codex.Startup.initGlosses=initGlosses;
	
	function go_online(evt){return offline_update();}
	function offline_update(){
	    Codex.writeGlosses();
	    var uri="https://"+Codex.server+
		"/v1/loadinfo.js?REFURI="+encodeURIComponent(Codex.refuri);
	    if (Codex.sync) uri=uri+"&SYNC="+(Codex.sync+1);
	    fdjtAjax.jsonCall(Codex.loadInfo,uri);}
	function offline_import(results){
	    fdjtKB.Import(results);
	    var i=0; var lim=results.length;
	    var syncstamp=Codex.syncstamp; var tstamp=false;
	    while (i<lim) {
		tstamp=results[i++].tstamp;
		if ((tstamp)&&(tstamp>syncstamp)) syncstamp=tstamp;}
	    Codex.syncstamp=syncstamp;
	    setLocal("syncstamp("+Codex.refuri+")",syncstamp);}
	Codex.update=offline_update;
	
	fdjtDOM.addListener(window,"online",go_online);

	function initState() {
	    var uri=Codex.docuri||Codex.refuri;
	    var statestring=getLocal("sbooks.state("+uri+")");
	    if (statestring) Codex.state=state=JSON.parse(statestring);}
	
	/* This initializes the sbook state to the initial location with the
	   document, using the hash value if there is one. */ 
	function initLocation() {
	    var state=false;
	    if (!(state)) {
		var uri=Codex.docuri||Codex.refuri;
		var statestring=getLocal("sbooks.state("+uri+")");
		if (statestring) Codex.state=state=JSON.parse(statestring);
		else state={};}
	    var hash=window.location.hash; var target=false;
	    if ((typeof hash === "string") && (hash.length>0)) {
		if ((hash[0]==='#') && (hash.length>1))
		    target=document.getElementById(hash.slice(1));
		else target=document.getElementById(hash);
		if (Codex.Trace.startup>1)
		    fdjtLog("sbookInitLocation hash=%s=%o",hash,target);}
	    if (target) Codex.GoTo(target,false,true);
	    else if ((state)&&(state.target)&&(fdjtID(state.target)))
		Codex.GoTo(state.target,false,true);
	    else Codex.GoTo((Codex.start||Codex.coverpage||Codex.titlepage||Codex.root),false,true);
	    if ((Codex.user)&&(Codex.dosync)&&(navigator.onLine))
		syncLocation();}
	
	function syncLocation() {
	    if (!(Codex.user)) return;
	    var uri="https://"+Codex.server+"/v1/sync"+
		"?DOCURI="+encodeURIComponent(Codex.docuri)+
		"&REFURI="+encodeURIComponent(Codex.refuri);
	    if (Codex.Trace.dosync)
		fdjtLog("syncLocation(call) %s",uri);
	    fdjtAjax.jsonCall(
		function(d){
		    if (Codex.Trace.dosync)
			fdjtLog("syncLocation(callback) %s: %j",uri,d);
		    if ((!(d))||(!(d.location))) {
			if (!(Codex.state))
			    Codex.GoTo(Codex.start||Codex.root||Codex.body,false,false);
			return;}
		    else if ((!(Codex.state))||(Codex.state.tstamp<d.tstamp)) {
			if ((d.location)&&(d.location<Codex.location)) return;
			var msg=
			    "Sync to L"+d.location+
			    ((d.page)?(" (page "+d.page+")"):"")+"?";
			if (confirm(msg)) {
			    if (d.location) Codex.setLocation(d.location);
			    if (d.target) Codex.setTarget(d.target,true,true);
			    if (d.location) Codex.GoTo(d.location,true,true);
			    Codex.state=d;}}
		    else {}},

		uri);}

	function applyInlineTags(){
	    startupMessage("Applying inline tags");
	    var tags=fdjtDOM.$(".sbooktags");
	    var i=0; var lim=tags.length;
	    while (i<lim) {
		var tagelt=tags[i++];
		var target=Codex.getTarget(tagelt);
		var info=Codex.docinfo[target.id];
		var tagtext=fdjtDOM.textify(tagelt);
		var tagsep=tagelt.getAttribute("tagsep")||";";
		var tagstrings=tagtext.split(tagsep);
		if (tagstrings.length) {
		    if (info.tags)
			info.tags=info.tags.concat(tagstrings);
		    else info.tags=tagstrings;}}}
	
	/* Indexing tags */

	function indexContentTags(docinfo){
	    var sbook_index=Codex.index;
	    knodule=(knodule)||(knodule=Codex.knodule);
	    /* One pass processes all of the inline KNodes and
	       also separates out primary and auto tags. */
	    for (var eltid in docinfo) {
		var tags=docinfo[eltid].tags;
		if (!(tags)) continue;
		var k=0; var ntags=tags.length; var scores=tags.scores||false;
		if (!(scores)) tags.scores=scores={};
		while (k<ntags) {
		    var tag=tags[k]; var score=1; var tagbase=false;
		    if (tag[0]==='*') {
			var tagstart=tag.search(/[^*]+/);
			score=2*(tagstart+1);
			tagbase=tag.slice(tagstart);}
		    else if (tag[0]==='~') {
			var tagstart=tag.search(/[^~]+/);
			tag=tag.slice(tagstart);
			if (tagstart>1) {
			    if (!(scores)) tags.scores=scores={};
			    score=1/tagstart;}
			else score=1;}
		    else {
			tagbase=tag;
			score=2;}
		    if (tagbase) {
			var knode=((tagbase.indexOf('|')>0)?
				   (knodule.handleSubjectEntry(tagbase)):
				   (knodule.ref(tagbase)));
			if ((knode)&&(knode.tagString)) tag=knode.tagString();}
		    tags[k]=tag;
		    scores[tag]=score;
		    k++;}
		if (scores) {
		    tags.sort(function(t1,t2){
			var s1=scores[t1]||1; var s2=scores[t2]||1;
			if (s1>s2) return -1;
			else if (s1<s2) return 1;
			else if (t1<t2) return -1;
			else if (t1>t2) return 1;
			else return 0;});}
		else tags.sort();}
	    var knodule=Codex.knodule||false;
	    sbook_index.Tags=function(item){
		if (docinfo[item]) {
		    var info=docinfo[item];
		    if (info.alltags) return info.alltags;
		    return (info.alltags=
			    (KnoduleIndex.combineTags([info.tags||false,
						       info.glosstags||false,
						       info.autotags||false])));}
		var info=Codex.glosses.ref(item)||fdjtKB.ref(item);
		return ((info)&&(info.tags))||[];};
	    for (var eltid in docinfo) {
		var tags=docinfo[eltid].tags; 
		if (!(tags)) continue;
		var scores=tags.scores;
		var k=0; var ntags=tags.length;
		while (k<ntags) {
		    var tag=tags[k++];
		    if (scores)
			sbook_index.add(eltid,tag,scores[tag]||1,knodule);
		    else sbook_index.add(eltid,tag,1,knodule);}}}
	Codex.indexContentTags=indexContentTags
	
	/* Inline tags */
	function indexInlineTags(kno) {
	    var sbook_index=Codex.index;
	    if (!(kno)) kno=knodule;
	    var anchors=document.getElementsByTagName("A");
	    if (!(anchors)) return;
	    var i=0; var len=anchors.length;
	    while (i<len)
		if (anchors[i].rel==='tag') {
		    var elt=anchors[i++];
		    var href=elt.href;
		    var cxt=elt;
		    while (cxt) if (cxt.id) break; else cxt=cxt.parentNode;
		    if (!((href)&&(cxt))) return;
		    var tagstart=(href.search(/[^/]+$/));
	    var tag=((tagstart<0)?(href):href.slice(tagstart));
	    var dterm=((kno)?(kno.handleEntry(tag)):(fdjtString.stdspace(tag)));
	    sbook_index.add(cxt,dterm);}
	else i++;}
     Codex.indexInlineTags=indexInlineTags;

     function useAutoIndex(autoindex,knodule){
	 var sbook_index=Codex.index;
	 if (!(autoindex)) return;
	 if (!(sbook_index)) return;
	 for (var tag in autoindex) {
	     var ids=autoindex[tag];
	     var starpower=tag.search(/[^*]/);
	     // all stars or empty string, just ignore
	     if (starpower<0) continue;
	     var weight=((tag[0]==='~')?(1):(2*(starpower+1)));
	     var knode=((tag.indexOf('|')>=0)?
			(knodule.handleSubjectEntry(tag.slice(starpower))):
			(tag[0]==='~')?(tag.slice(1)):
			(knodule.handleSubjectEntry(tag.slice(starpower))));
	     var i=0; var lim=ids.length;
	     while (i<lim) {
		 var info=Codex.docinfo[ids[i++]];
		 if (!(info)) continue;
		 var tagval=((typeof knode === 'string')?(knode):(knode.dterm));
		 if (info.autotags) info.autotags.push(tagval);
		 else info.autotags=[tagval];
		 sbook_index.add(info.frag,knode,weight,knodule);}}}
     Codex.useAutoIndex=useAutoIndex;

     /* Setting up the clouds */

     function initClouds(){
	 startupMessage("setting up search cloud...");
	 fdjtDOM.replace("CODEXSEARCHCLOUD",Codex.fullCloud().dom);
	 startupMessage("setting up glossing cloud...");
	 fdjtDOM.replace("CODEXGLOSSCLOUD",Codex.glossCloud().dom);
	 if (Codex.cloud_queue) {
	     fdjtLog("Starting to sync gloss cloud");
	     fdjtTime.slowmap(
		 Codex.addTag2UI,Codex.cloud_queue,false,
		 function(){
		     Codex.cloud_queue=false;
		     fdjtLog("Gloss cloud synced");});}
	 if (Codex.search_cloud_queue) {
	     fdjtLog("Starting to sync search cloud");
	     fdjtTime.slowmap(
		 Codex.addTag2UI,Codex.search_cloud_queue,false,
		 function(){
		     Codex.search_cloud_queue=false;
		     fdjtLog("Search cloud synced");});}
	 
	 if (Codex.knodule) {
	     fdjtLog("Beginning knodule integration");
	     fdjtTime.slowmap(Codex.addTag2UI,Codex.knodule.alldterms,false,
			      function(){fdjtLog("Knodule integrated");});}
	 Codex.sizeCloud(Codex.full_cloud);
	 Codex.sizeCloud(Codex.gloss_cloud);}
     
     /* Clearing offline data */

     function clearOffline(refuri){
	 var dropLocal=fdjtState.dropLocal;
	 if (refuri) {
	     var glosses=getLocal("glosses("+refuri+")",true);
	     var i=0; var lim=glosses.length;
	     while (i<lim) fdjtState.dropLocal(glosses[i++]);
	     dropLocal("sources("+refuri+")");
	     dropLocal("outlets("+refuri+")");
	     dropLocal("queued("+refuri+")");
	     dropLocal("sync("+refuri+")");
	     dropLocal("user("+refuri+")");
	     dropLocal("sync("+refuri+")");
	     dropLocal("etc("+refuri+")");
	     dropLocal("offline("+refuri+")");
	     var refuris=getLocal("sbooks.refuris",true);
	     refuris=fdjtKB.remove(refuris,refuri);
	     setLocal("sbooks.refuris",refuris,true);}
	 else {
	     var refuris=getLocal("sbooks.refuris",true);
	     var i=0; var lim=refuris.length;
	     while (i<lim) clearOffline(refuris[i++]);
	     dropLocal("sbooks.refuris");}}
     Codex.clearOffline=clearOffline;

     /* Other setup */
     
     function setupGlossServer(){}

     Codex.StartupHandler=function(evt){
	 Startup();};

     return Startup;})();
sbookStartup=Codex.StartupHandler;
Codex.Setup=Codex.StartupHandler;
sbook={Start: Codex.StartupHandler,setUser: Codex.setUser};

fdjt_versions.decl("codex",codex_startup_version);
fdjt_versions.decl("codex/startup",codex_startup_version);

/* Emacs local variables
   ;;;  Local variables: ***
   ;;;  compile-command: "cd ..; make" ***
   ;;;  End: ***
*/
/* -*- Mode: Javascript; Character-encoding: utf-8; -*- */

var codex_domscan_id="$Id$";
var codex_domscan_version=parseInt("$Revision$".slice(10,-1));

/* Copyright (C) 2009-2011 beingmeta, inc.
   This file implements a Javascript/DHTML UI for reading
   large structured documents (sBooks).

   For more information on sbooks, visit www.sbooks.net
   For more information on knodules, visit www.knodules.net
   For more information about beingmeta, visit www.beingmeta.com

   This library uses the FDJT (www.fdjt.org) toolkit.

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

   Use and redistribution (especially embedding in other
   CC licensed content) is permitted under the terms of the
   Creative Commons "Attribution-NonCommercial" license:

   http://creativecommons.org/licenses/by-nc/3.0/ 

   Other uses may be allowed based on prior agreement with
   beingmeta, inc.  Inquiries can be addressed to:

   licensing@beingmeta.com

   Enjoy!

*/

/* Scanning the document for Metadata */

function CodexDOMScan(root,docinfo){
    var stdspace=fdjtString.stdspace;
    if (typeof root === 'undefined') return this;
    if (!(docinfo))
	if (this instanceof CodexDOMScan)
	    docinfo=this;
    else docinfo=new CodexDOMScan();
    if (!(root)) root=Codex.root||document.body;
    var start=new Date();
    var allheads=[];
    docinfo._root=root;
    docinfo._heads=allheads;
    if (!(root.id)) root.id="SBOOKROOT";
    if (Codex.Trace.startup) {
	if (root.id) 
	    fdjtLog("Scanning %s#%s for structure and metadata",root.tagName,root.id);
	else fdjtLog("Scanning DOM for structure and metadata: %o",root);}
    var nodefn=docinfo.nodeFn||false;
    var children=root.childNodes, level=false;
    var scanstate=
	{curlevel: 0,idserial:0,location: 0,
	 nodecount: 0,eltcount: 0,headcount: 0,
	 tagstack: [],taggings: [],allinfo: [],locinfo: [],
	 idstate: {prefix: false,count: 0},
	 idstack: [{prefix: false,count: 0}],
	 pool: Codex.DocInfo};
    var rootinfo=(((nodefn)&&(nodeFn(root)))||(docinfo[root.id])||
		  (docinfo[root.id]=new scanInfo(root.id,scanstate)));
    scanstate.curhead=root; scanstate.curinfo=rootinfo;
    // Location is an indication of distance into the document
    var location=0;
    rootinfo.pool=scanstate.pool;
    rootinfo.title=root.title||document.title;
    rootinfo.starts_at=0;
    rootinfo.level=0; rootinfo.sub=new Array();
    rootinfo.head=false; rootinfo.heads=new Array();
    rootinfo.frag=root.id;
    rootinfo._id="#"+root.id;
    rootinfo.elt=root;
    scanstate.allinfo.push(rootinfo);
    scanstate.allinfo.push(0);
    /* Build the metadata */
    var i=0; while (i<children.length) {
	var child=children[i++];
	if (!((child.sbookskip)||(child.codexui)))
	    scanner(child,scanstate,docinfo,docinfo.nodeFn||false);} 
    docinfo._nodecount=scanstate.nodecount;
    docinfo._headcount=scanstate.headcount;
    docinfo._eltcount=scanstate.eltcount;
    docinfo._maxloc=scanstate.location;
    docinfo._allinfo=scanstate.allinfo;
    docinfo._locinfo=scanstate.locinfo;
    var scaninfo=scanstate.curinfo;
    /* Close off all of the open spans in the TOC */
    while (scaninfo) {
	scaninfo.ends_at=scanstate.location;
	scaninfo=scaninfo.head;}
    var done=new Date();
    if (Codex.Trace.startup)
	fdjtLog('Gathered metadata in %f secs over %d/%d heads/nodes',
		(done.getTime()-start.getTime())/1000,
		scanstate.headcount,scanstate.eltcount);
    return docinfo;

    function scanInfo(id,scanstate) {
	if (docinfo[id]) return docinfo[id];
	this.pool=scanstate.pool;
	this.frag=id;
	this._id="#"+id;
	docinfo[id]=this;
	scanstate.allinfo.push(this);
	scanstate.locinfo.push(scanstate.location);
	return this;}
    CodexDOMScan.scanInfo=scanInfo;

    function getTitle(head) {
	var title=
	    (head.toctitle)||
	    ((head.getAttributeNS)&&
	     (head.getAttributeNS('toctitle','http://sbooks.net')))||
	    (head.getAttribute('toctitle'))||
	    (head.getAttribute('data-toctitle'))||
	    (head.title);
	if (!(title)) title=gatherText(head);
	if (typeof title === "string") {
	    var std=stdspace(title);
	    if (std==="") return false;
	    else return std;}
	else return fdjtDOM.textify(title,true);}

    function gatherText(head,s) {
	if (!(s)) s="";
	if (head.nodeType===3)
	    return s+head.nodeValue;
	else if (head.nodeType!==1) return s;
	else {
	    var children=head.childNodes;
	    var i=0; var len=children.length;
	    while (i<len) {
		var child=children[i++];
		if (child.nodeType===3) s=s+child.nodeValue;
		else if (child.nodeType===1)
		    s=gatherText(child,s);
		else {}}
	    return s;}}

    function textWidth(elt){
	if (elt.nodeType===3) return elt.nodeValue.length;
	else if (elt.nodeType===1) {
	    var children=elt.childNodes; var loc=0;
	    var i=0; var len=children.length;
	    while (i<len) {
		var child=children[i++];
		if (child.nodeType===3) loc=loc+child.nodeValue.length;
		else if (child.nodeType===1)
		    loc=loc+textWidth(child);
		else {}}
	    return loc;}
	else return 0;}

    function getLevel(elt){
	if (elt.toclevel) {
	    if (elt.toclevel==='none')
		return elt.toclevel=false;
	    else return elt.toclevel;}
	var attrval=
	    ((elt.getAttributeNS)&&
	     (elt.getAttributeNS('toclevel','http://sbooks.net')))||
	    (elt.getAttribute('toclevel'))||
	    (elt.getAttribute('data-toclevel'));
	if (attrval) {
	    if (attrval==='none') return false;
	    else return parseInt(attrval);}
	if (elt.className) {
	    var cname=elt.className;
	    if (cname.search(/\bsbooknotoc\b/)>=0) return 0;
	    if (cname.search(/\bsbookignore\b/)>=0) return 0;
	    var tocloc=cname.search(/\bsbook\dhead\b/);
	    if (tocloc>=0) return parseInt(cname.slice(5,6));}
	if ((Codex.notoc)&&(Codex.notoc.match(elt))) return 0;
	if ((Codex.ignore)&&(Codex.ignore.match(elt))) return 0;
	if ((elt.tagName==='HGROUP')||(elt.tagName==='HEADER'))
	    return getFirstTocLevel(elt,true);
	if (elt.tagName.search(/H\d/)==0)
	    return parseInt(elt.tagName.slice(1,2));
	else return false;}

    function getFirstTocLevel(node,notself){
	if (node.nodeType!==1) return false;
	var level=((!(notself))&&(getLevel(node)));
	if (level) return level;
	var children=node.childNodes;
	var i=0; var lim=children.length;
	while (i<lim) {
	    var child=children[i++];
	    if (child.nodeType!==1) continue;
	    level=getFirstTocLevel(child);
	    if (level) return level;}
	return false;}

    function handleHead(head,docinfo,scanstate,level,curhead,curinfo,curlevel,nodefn){
	var headid=head.id;
	var headinfo=((nodefn)&&(nodefn(head)))||docinfo[headid]||
	    (docinfo[headid]=new scanInfo(headid,scanstate));
	scanstate.headcount++;
	allheads.push(head);
	if ((headinfo.elt)&&(headinfo.elt!==head)) {
	    var newid=headid+"x"+scanstate.location;
	    fdjtLog.warn("Duplicate ID=%o newid=%o",headid,newid);
	    head.id=headid=newid;
	    headinfo=((nodefn)&&(nodefn(head)))||docinfo[headid]||
		(docinfo[headid]=new scanInfo(headid,scanstate));}
	if (Codex.Trace.scan)
	    fdjtLog("Scanning head item %o under %o at level %d w/id=#%s ",
		    head,curhead,level,headid);
	/* Iniitalize the headinfo */
	headinfo.starts_at=scanstate.location;
	headinfo.elt=head; headinfo.level=level;
	headinfo.sub=new Array();
	headinfo.frag=headid; headinfo._id="#"+headid;
	headinfo.title=getTitle(head);
	headinfo.next=false; headinfo.prev=false;
	if (level>curlevel) {
	    /* This is the simple case where we are a subhead
	       of the current head. */
	    headinfo.head=curinfo;
	    if (!(curinfo.intro_ends_at))
		curinfo.intro_ends_at=scanstate.location;
	    curinfo.sub.push(headinfo);}
	else {
	    /* We're not a subhead, so we're popping up at least one level. */
	    var scan=curhead;
	    var scaninfo=curinfo;
	    var scanlevel=curinfo.level;
	    /* Climb the stack of headers, closing off entries and setting up
	       prev/next pointers where needed. */
	    while (scaninfo) {
		if (Codex.Trace.scan)
		    fdjtLog("Finding head: scan=%o, info=%o, sbook_head=%o, cmp=%o",
			    scan,scaninfo,scanlevel,scaninfo.head,
			    (scanlevel<level));
		if (scanlevel<level) break;
		if (level===scanlevel) {
		    headinfo.prev=scaninfo;
		    scaninfo.next=headinfo;}
		scaninfo.ends_at=scanstate.location;
		scanstate.tagstack=scanstate.tagstack.slice(0,-1);
		scaninfo=scaninfo.head; scan=scaninfo.elt;
		scanlevel=((scaninfo)?(scaninfo.level):(0));}
	    if (Codex.Trace.scan)
		fdjtLog("Found parent: up=%o, upinfo=%o, atlevel=%d, sbook_head=%o",
			scan,scaninfo,scaninfo.level,scaninfo.head);
	    /* We've found the head for this item. */
	    headinfo.head=scaninfo;
	    scaninfo.sub.push(headinfo);} /* handled below */
	/* Add yourself to your children's subsections */
	var supinfo=headinfo.head;
	var newheads=new Array();
	if (supinfo.heads)
	    newheads=newheads.concat(supinfo.heads);
	if (supinfo) newheads.push(supinfo);
	headinfo.heads=newheads;
	if (Codex.Trace.scan)
	    fdjtLog("@%d: Found head=%o, headinfo=%o, sbook_head=%o",
		    scanstate.location,head,headinfo,headinfo.head);
	/* Update the toc state */
	scanstate.curhead=head;
	scanstate.curinfo=headinfo;
	scanstate.curlevel=level;
	if (headinfo)
	    headinfo.ends_at=scanstate.location+fdjtDOM.textWidth(head);
	scanstate.location=scanstate.location+fdjtDOM.textWidth(head);}

    function scanner(child,scanstate,docinfo,nodefn){
	var location=scanstate.location;
	var curhead=scanstate.curhead;
	var curinfo=scanstate.curinfo;
	var curlevel=scanstate.curlevel;
	scanstate.nodecount++;
	// Location tracking and TOC building
	if (child.nodeType===3) {
	    var content=stdspace(child.nodeValue);
	    var width=content.length;
	    // Need to regularize whitespace
	    scanstate.location=scanstate.location+width;
	    return 0;}
	else if (child.nodeType!==1) return 0;
	else {}
	if ((Codex.ignore)&&(Codex.ignore.match(child))) return;
	// Having a section inside a notoc zone probably indicates malformed
	//  HTML
	if (((child.tagName==='section')||(child.tagName==='article'))&&
	    (!(scanstate.notoc))) {
	    var head=fdjtDOM.findChild(child,'header')||
		fdjtDOM.findChild(child,'hgroup,h1,h2,h3,h4,h5,h6,h7');
	    var curlevel=scanstate.curlevel;
	    var curhead=scanstate.curhead;
	    var curinfo=scanstate.curinfo;
	    var notoc=scanstate.notoc;
	    var header=fdjtDOM.getChild(child,"header");
	    var nextlevel=getLevel(child)||
		getFirstTocLevel(header)||
		getFirstTocLevel(child)||
		((curlevel)?(curlevel+1):(1));
	    handleHead(child,docinfo,scanstate,nextlevel,
		       curhead,curinfo,curlevel,
		       nodefn);
	    if ((Codex.terminals)&&(Codex.terminals.match(child)))
		scanstate.notoc=true;
	    var headinfo=docinfo[child.id];
	    headinfo.tocdone=true;
	    scanstate.curhead=child; scanstate.curinfo=headinfo;
	    scanstate.curlevel=nextlevel;
	    var children=child.childNodes;
	    var i=0; var lim=children.length;
	    while (i<lim) {
		var child=children[i++];
		if (child.nodeType===1)
		    scanner(child,scanstate,docinfo,nodefn);}
	    // Put everything back
	    scanstate.curlevel=curlevel; scanstate.notoc=notoc;
	    scanstate.curhead=curhead; scanstate.curinfo=curinfo;
	    return;}
	// Get the location in the TOC for this out of context node
	var tocloc=(child.codextocloc)||(child.getAttribute("data-tocloc"));
	if ((tocloc)&&(docinfo[tocloc])) {
	    var tocinfo=docinfo[tocloc];
	    var curlevel=scanstate.curlevel;
	    var curhead=scanstate.curhead;
	    var curinfo=scanstate.curinfo;
	    var notoc=scanstate.notoc;
	    var headinfo=tocinfo.head;
	    scanstate.curinfo=headinfo;
	    scanstate.curhead=headinfo.elt;
	    scanstate.curlevel=headinfo.level;
	    scanstate.notoc=true;
	    var children=child.childNodes;
	    var i=0; var lim=children.length;
	    while (i<lim) {
		var child=children[i++];
		if (child.nodeType===1)
		    scanner(child,scanstate,docinfo,nodefn);}
	    // Put everything back
	    scanstate.curlevel=curlevel; scanstate.notoc=notoc;
	    scanstate.curhead=curhead; scanstate.curinfo=curinfo;
	    return;}
	var toclevel=((child.id)&&(getLevel(child)));
	// The header functionality (for its contents too) is handled by the
	// section
	if ((scanstate.notoc)||(child.tagName==='header')) {
	    scanstate.notoc=true; toclevel=0;}
	scanstate.eltcount++;
	var info=((nodefn)&&(nodefn(child)));
	if ((!(info))&&(child.id)&&(!(info=docinfo[child.id]))) {
	    var id=child.id;
	    info=new scanInfo(id,scanstate);}
	if ((info)&&(info.elt)&&(child.id)&&(info.elt!==child)) {
	    var newid=child.id+"x"+scanstate.location;
	    fdjtLog.warn("Duplicate ID=%o newid=%o",child.id,newid);
	    child.id=id=newid;
	    info=((nodefn)&&(nodefn(head)))||docinfo[id]||
		(docinfo[id]=new scanInfo(id,scanstate));}
	if (info) {
	    info.starts_at=scanstate.location;
	    info.sbookhead=curhead.id;
	    info.headstart=curinfo.starts_at;}
	if (info) info.head=curinfo;
	if ((child.sbookskip)||(child.codexui)||
	    ((child.className)&&(child.className.search(/\bsbookignore\b/)>=0))||
	    ((Codex.ignore)&&(Codex.ignore.match(child))))
	    return;
	if ((info)&&(toclevel)&&(!(info.toclevel))) info.toclevel=toclevel;
	if (child.id) {
	    var tags=
		((child.getAttributeNS)&&
		 (child.getAttributeNS('tags','http://sbooks.net/')))||
		(child.getAttribute('tags'))||
		(child.getAttribute('data-tags'));
	    if (tags) info.tags=tags.split(';');}
	if ((toclevel)&&(!(info.tocdone)))
	    handleHead(child,docinfo,scanstate,toclevel,
		       curhead,curinfo,curlevel,nodefn);
	var children=child.childNodes;
	var i=0; var len=children.length;
	while (i<len) {
	    var grandchild=children[i++];
	    if (grandchild.nodeType===3) {
		var content=stdspace(grandchild.nodeValue);
		scanstate.location=scanstate.location+
		    content.length;}
	    else if (grandchild.nodeType===1) {
		scanner(grandchild,scanstate,docinfo,nodefn);}}
	if (info) info.ends_at=scanstate.location;}}

fdjt_versions.decl("codex",codex_domscan_version);
fdjt_versions.decl("codex/domscan",codex_domscan_version);

/* Emacs local variables
   ;;;  Local variables: ***
   ;;;  compile-command: "cd ..; make" ***
   ;;;  End: ***
*/
/* -*- Mode: Javascript; Character-encoding: utf-8; -*- */

var codex_hud_id="$Id$";
var codex_hud_version=parseInt("$Revision$".slice(10,-1));

/* Copyright (C) 2009-2011 beingmeta, inc.
   This file implements a Javascript/DHTML UI for reading
   large structured documents (sBooks).

   For more information on sbooks, visit www.sbooks.net
   For more information on knodules, visit www.knodules.net
   For more information about beingmeta, visit www.beingmeta.com

   This library uses the FDJT (www.fdjt.org) toolkit.

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

   Use and redistribution (especially embedding in other
   CC licensed content) is permitted under the terms of the
   Creative Commons "Attribution-NonCommercial" license:

   http://creativecommons.org/licenses/by-nc/3.0/ 

   Other uses may be allowed based on prior agreement with
   beingmeta, inc.  Inquiries can be addressed to:

   licensing@beingmeta.com

   Enjoy!

*/

var CodexMode=
    (function(){
	// The foot HUD
	var sbookHead=false; var head_height=false;
	// The foot HUD
	var sbookFoot=false; var foot_height=false;
	// The HELP HUD, and its margins
	var sbookHelp=false; var help_top=false; var help_bottom=false;
	// The BOX HUD (contains scrollable content) and its margins
	var box_top=false; var box_bottom=false;
	// This is the HUD where all glosses are displayed
	var sbookGlossesHUD=false;
	// This is the HUD for tag searching
	var sbookSearchHUD=false;
	// How long to let messages flash up
	var message_timeout=5000;
	// Whether to call displaySync on mode changes
	var display_sync=false;
	
	var addClass=fdjtDOM.addClass;
	var dropClass=fdjtDOM.dropClass;
	var hasClass=fdjtDOM.hasClass;

	// This will contain the interactive input console (for debugging)
	var input_console;

	function initHUD(){
	    if (fdjtID("CODEXHUD")) return;
	    else {
		Codex.HUD=CodexHUD=fdjtDOM("div#CODEXHUD");
		CodexHUD.codexui=true;
		CodexHUD.innerHTML=sbook_hudtext;
		fdjtDOM.prepend(document.body,fdjtID("HUMANE"),CodexHUD);}
	    // Setup flyleaf
	    var flyleaf=fdjtID("CODEXFLYLEAF");
	    flyleaf.innerHTML=sbook_flyleaftext;
	    // Setup settings
	    var settings=fdjtID("CODEXSETTINGS");
	    settings.innerHTML=sbook_settingstext;
	    // Setup help text
	    var help=fdjtID("CODEXHELP");
	    help.innerHTML=sbook_helptext;
	    // Initialize search UI
	    var search=fdjtID("CODEXSEARCH");
	    search.innerHTML=sbook_searchbox;
	    Codex.empty_cloud=
		new fdjtUI.Completions(fdjtID("CODEXSEARCHCLOUD"));
	    // Setup addgloss prototype
	    var addgloss=fdjtID("CODEXADDGLOSSPROTOTYPE");
	    addgloss.innerHTML=sbook_addgloss;

	    if (Codex.hidehelp) Codex.setConfig("hidehelp");

	    fdjtID("SBOOK_RETURN_TO").value=location.href;

	    // Iniitialize the ABOUT tab
	    

	    // Initialize gloss UI
	    var glosses=fdjtID("CODEXALLGLOSSES");
	    Codex.UI.setupSummaryDiv(glosses);
	    Codex.glosses.addEffect("maker",function(f,p,v){
		Codex.sourcekb.ref(v).oninit
		(Codex.UI.addGlossSource,"newsource");});
	    Codex.glosses.addEffect("sources",function(f,p,v){
		Codex.sourcekb.ref(v).oninit
		(Codex.UI.addGlossSource,"newsource");});

	    function addGloss2UI(item){
		if (document.getElementById(item.frag)) {
		    var addGlossmark=Codex.UI.addGlossmark;
		    Codex.UI.addToSlice(item,glosses,false);
		    var node=fdjtID(item.frag);
		    if (node) {
			addClass(node,"glossed");
			addGlossmark(node);}
		    var dups=((Codex.paginated)&&(Codex.paginated.dups)&&
			      (Codex.paginated.dups[item.frag]))
		    if (dups) {
			var i=0, lim=dups.length;
			while (i<lim) {
			    var dup=dups[i++];
			    addClass(dup,"glossed");
			    addGlossmark(dup);}}
		    if (item.tstamp>Codex.syncstamp)
			Codex.syncstamp=item.tstamp;
		    if (item.tags) addTag2UI(item.tags,true);}}
	    Codex.glosses.addInit(addGloss2UI);

	    function addTag2UI(tag,forsearch){
		if (!(tag)) return;
		else if (tag instanceof Array) {
		    var i=0; var lim=tag.length;
		    while (i<lim) addTag2UI(tag[i++],forsearch||false);
		    return;}
		else if (!(Codex.gloss_cloud)) {
		    var queue=Codex.cloud_queue;
		    if (!(queue)) queue=Codex.cloud_queue=[];
		    queue.push(tag);
		    if (forsearch) {
			var squeue=Codex.search_cloud_queue;
			if (!(squeue)) squeue=Codex.search_cloud_queue=[];
			squeue.push(tag);}}
		else {
		    var gloss_cloud=Codex.glossCloud();
		    var search_cloud=Codex.fullCloud();
		    var gloss_tag=gloss_cloud.getByValue(tag,".completion");
		    if (!((gloss_tag)&&(gloss_tag.length))) {
			if (!((typeof tag === 'string')&&
			      (!(Codex.knodule.probe(tag))))) {
			    gloss_tag=Knodule.HTML(tag,Codex.knodule,false,true);
			    fdjtDOM(fdjtID("CODEXGLOSSTAGS"),gloss_tag," ");
			    gloss_cloud.addCompletion(gloss_tag);}}
		    var search_tag=
			((forsearch)&&
			 (search_cloud.getByValue(tag,".completion")));
		    if ((forsearch)&&(!((search_tag)&&(search_tag.length)))) {
			search_tag=Knodule.HTML(tag,Codex.knodule,false,true);
			fdjtDOM(fdjtID("CODEXSEARCHTAGS"),search_tag," ");
			search_cloud.addCompletion(search_tag);}}}
	    Codex.addTag2UI=addTag2UI;
	    
	    var console=fdjtID("CODEXCONSOLE");
	    input_console=fdjtDOM.getChild(console,"TEXTAREA");
	    input_button=fdjtDOM.getChild(console,"span.button");
	    input_button.onclick=consolebutton_click;
	    input_console.onkeypress=consoleinput_keypress;

	    sbookFoot=fdjtID("CODEXFOOT");
	    sbookHead=fdjtID("CODEXHEAD");
	    sbookHelp=fdjtID("CODEXHELP");
	    fillinFlyleaf();
	    resizeHUD();
	    Codex.scrollers={};
	    // updateScroller("CODEXGLOSSCLOUD");
	    updateScroller("CODEXSEARCHCLOUD");}
	Codex.initHUD=initHUD;
	
	function fixStaticRefs(string){
	  if (Codex.graphics==="http://static.beingmeta.com/graphics/")
	    return string;
	  else return string.replace
		 (/http:\/\/static.beingmeta.com\/graphics\//g,
		  Codex.graphics);}

	function resizeHUD(){
	    var vh=fdjtDOM.viewHeight();
	    var vw=fdjtDOM.viewWidth();
	    var hf=fdjtID("CODEXFOOT");
	    var fh=fdjtDOM.getGeometry(hf).height;
	    // fdjtLog("resizeHUD vh=%o vw=%o fh=%o",vh,vw,fh);
	    if (!(Codex.nativescroll)) hf.style.top=(vh-fh)+'px';}

	/* This is used for viewport-based browser, where the HUD moves
	   to be aligned with the viewport */
	
	var sbook_sync_off=false;
	var sbook_sync_height=false;
	
	function getBounds(elt){
	    var style=fdjtDOM.getStyle(elt);
	    return { top: fdjtDOM.parsePX(style.marginTop)||0+
		     fdjtDOM.parsePX(style.borderTop)||0+
		     fdjtDOM.parsePX(style.paddingTop)||0,
		     bottom: fdjtDOM.parsePX(style.marginBottom)||0+
		     fdjtDOM.parsePX(style.borderBottom)||0+
		     fdjtDOM.parsePX(style.paddingBottom)||0};}
	fdjtDOM.getBounds=getBounds;
	
	/* Creating the HUD */
	
	function setupTOC(root_info){
	    var navhud=createNavHUD("div#CODEXTOC.hudpanel",root_info);
	    var toc_button=fdjtID("CODEXTOCBUTTON");
	    toc_button.style.visibility='';
	    Codex.TOC=navhud;
	    fdjtDOM.replace("CODEXTOC",navhud);
	    var flytoc=createStaticTOC("div#CODEXFLYTOC",root_info);
	    Codex.Flytoc=flytoc;
	    fdjtDOM(fdjtID("FLYTOC"),flytoc);}
	Codex.setupTOC=setupTOC;

	function createNavHUD(eltspec,root_info){
	    var toc_div=CodexTOC(root_info,0,false,"CODEXTOC4",
				 ((root_info.sub.length>1)));
	    var div=fdjtDOM(eltspec||"div#CODEXTOC.hudpanel",toc_div);
	    Codex.UI.addHandlers(div,"toc");
	    return div;}

	function createStaticTOC(eltspec,root_info){
	    var toc_div=CodexTOC(root_info,0,false,"CODEXFLYTOC4");
	    var div=fdjtDOM(eltspec||"div#CODEXFLYTOC",toc_div);
	    Codex.UI.addHandlers(div,"toc");
	    return div;}

	/* HUD animation */

	function setHUD(flag,clearmode){
	    if (typeof clearmode === 'undefined') clearmode=true;
	    // clearmode=((Codex.mode!=='scanning')&&(Codex.mode!=='tocscan'));
	    if (Codex.Trace.gestures)
		fdjtLog("setHUD %o mode=%o hudup=%o bc=%o hc=%o",
			flag,Codex.mode,Codex.hudup,
			document.body.className,
			CodexHUD.className);
	    if (flag) {
		Codex.hudup=true;
		addClass(document.body,"hudup");}
	    else {
		Codex.hudup=false;
		Codex.scrolling=false;
		if (clearmode) {
		    var wait=false;
		    dropClass(CodexHUD,"flyleaf");
		    dropClass(CodexHUD,"full");
		    dropClass(CodexHUD,CodexMode_pat);
		    Codex.mode=false;}
		dropClass(document.body,"hudup");}}
	Codex.setHUD=setHUD;

	/* Mode controls */
	
	var CodexMode_pat=/\b((device)|(sbookapp)|(help)|(scanning)|(tocscan)|(search)|(searchresults)|(toc)|(glosses)|(allglosses)|(context)|(flytoc)|(about)|(console)|(minimal)|(addgloss)|(editexcerpt)|(gotoloc)|(gotopage))\b/g;
	var codexflyleafMode_pat=/\b((device)|(sbookapp)|(flytoc)|(about)|(console))\b/g;
	var sbook_mode_scrollers=
	    {allglosses: "CODEXALLGLOSSES",
	     searchresults: "CODEXSEARCHRESULTS",
	     search: "CODEXSEARCHCLOUD",
	     // addgloss: "CODEXGLOSSCLOUD",
	     console: "CODEXCONSOLE",
	     sbookapp: "MANAGEAPP",
	     flytoc: "CODEXFLYTOC",
	     about: "APPABOUT"};
	var sbook_mode_foci=
	    {gotopage: "CODEXPAGEINPUT",
	     gotoloc: "CODEXLOCINPUT",
	     search: "CODEXSEARCHINPUT"};
	
	var sbook_mode_help={
	    addgloss: "#CODEXADDGLOSSHELP",
	    toc: "#CODEXTOCHELP",
	    tocscan: "#CODEXTOCSCANHELP",
	    scanning: "#CODEXSCANNINGHELP"};

	function CodexMode(mode){
	    var oldmode=Codex.mode;
	    if (typeof mode === 'undefined') return oldmode;
	    if (mode==='last') mode=Codex.last_mode||'help';
	    if (mode==='none') mode=false;
	    if (mode==='flyleaf') mode=Codex.flyleaf_mode||"about";
	    if (Codex.Trace.mode)
		fdjtLog("CodexMode %o, cur=%o dbc=%o",
			mode,Codex.mode,document.body.className);
	    if ((Codex.mode==='help')&&(!(mode))) mode=Codex.last_mode;
	    if (mode) {
		if (mode!=="scanning") Codex.scanning=false;
		if ((mode==="scanning")||(mode==="tocscan"))
		    addClass(document.body,"sbookscanning");
		else dropClass(document.body,"sbookscanning");
		if (mode===Codex.mode) {}
		else if (mode===true) {
		    /* True just puts up the HUD with no mode info */
		    if (sbook_mode_foci[Codex.mode]) {
			var input=fdjtID(sbook_mode_foci[Codex.mode]);
			input.blur();}
		    Codex.mode=false;
		    Codex.last_mode=true;}
		else if (typeof mode !== 'string') 
		    throw new Error('mode arg not a string');
		else {
		    if (sbook_mode_foci[Codex.mode]) {
			var input=fdjtID(sbook_mode_foci[Codex.mode]);
			input.blur();}
		    if (sbook_mode_help[mode])
			fdjtLog.Humane(sbook_mode_help[mode]);
		    Codex.mode=mode;
		    if (Codex.mode!=='help') Codex.last_mode=Codex.mode;}
		// If we're switching to the inner app but the iframe
		//  hasn't been initialized, we do it now.
		if ((mode==="sbookapp")&&(!(fdjtID("MANAGEAPP").src)))
		    sbookSetupFlyleaf();
		// Update Codex.scrolling which is the scrolling
		// element in the HUD for this mode
		if (!(typeof mode === 'string'))
		    Codex.scrolling=false;
		else if (sbook_mode_scrollers[mode]) 
		    Codex.scrolling=(sbook_mode_scrollers[mode]);
		else Codex.scrolling=false;
		// Actually change the class on the HUD object
		if (mode===true) {
		    fdjtDOM.swapClass(CodexHUD,CodexMode_pat,"minimal");
		    dropClass(CodexHUD,"flyleaf");}
		else {
		    if (mode.search(codexflyleafMode_pat)<0)
			dropClass(CodexHUD,"flyleaf");
		    fdjtDOM.swapClass(CodexHUD,CodexMode_pat,mode);}
		// Update the body scanning mode
		if ((mode==="scanning")||(mode==="tocscan"))
		    addClass(document.body,"sbookscanning");
		else dropClass(document.body,"sbookscanning");
		// Update the 'flyleaf' meta mode
		if ((mode)&&(typeof mode === 'string')) {
		    if (mode.search(codexflyleafMode_pat)===0) {
			Codex.flyleaf_mode=mode;
			addClass(CodexHUD,"flyleaf");}
		    else dropClass(CodexHUD,"flyleaf");
		    fdjtID("CODEXBUTTON").className=mode;}
		// Help mode (on the hud) actually dims the body
		if (mode==="help")
		    addClass(document.body,"dimmed");
		else dropClass(document.body,"dimmed");
		// Scanning is a funny mode in that the HUD is down
		//  for it.  We handle all of this stuff here.
		if ((mode==='scanning')||(mode==='tocscan')) {
		    if (mode!==oldmode) {
			Codex.hudup=false;
			dropClass(CodexHUD,"flyleaf");
			dropClass(CodexHUD,"full");
			dropClass(document.body,"hudup");}}
		// And if we're not scanning, we just raise the hud
		else setHUD(true);
		// This updates scroller dimensions, we delay it
		//  because apparently, on some browsers, the DOM
		//  needs to catch up with CSS
		if (Codex.scrolling) {
		  var scroller=fdjtID(Codex.scrolling);
		  setTimeout(function(){updateScroller(scroller);},
			     100);}
		// If we're scanning all glosses, we sync the glosses
		//  with the current book location.
		if ((mode==="allglosses")&&(false)&&
		    (Codex.curinfo)&&(Codex.curinfo.first)) {
		    Codex.UI.scrollGlosses(
			Codex.curinfo.first,fdjtID("CODEXALLGLOSSES"));}
		// We autofocus any input element appropriate to the
		// mode
		if (sbook_mode_foci[mode]) {
		  var input=fdjtID(sbook_mode_foci[mode]);
		  if (input) focus(input);}
		// Moving the focus back to the body lets keys work
		else document.body.focus();
		if (display_sync) Codex.displaySync();}
	    else {
		// Clearing the mode is a lot simpler, in part because
		//  setHUD clears most of the classes when it brings
		//  the HUD down.
		fdjtLog.HumaneHide();
		if (Codex.mode!=='help') Codex.last_mode=Codex.mode;
		if (Codex.liveinput) {
		    Codex.liveinput.blur();
		    Codex.liveinput=false;}
		document.body.focus();
		dropClass(document.body,"dimmed");
		dropClass(document.body,"sbookscanning");
		setHUD(false);
		if (display_sync) Codex.displaySync();}}

	function focus(input){
	    input.focus(); Codex.liveinput=input;}
	Codex.setFocus=focus;

	function fadeUpHUD(){
	    fdjtLog("Setting properties");
	    CodexHUD.style.opacity=0.001;
	    setTimeout(function(){
		fdjtLog("Changing opacity");
		CodexHUD.style.opacity=1.00;
		setTimeout(function(){
		    fdjtLog("Clearing setup");
		    CodexHUD.style.opacity='';},
			   1500);},
		       1500);}
	Codex.fadeUpHUD=fadeUpHUD;

	function updateScroller(elt){
	    if (typeof elt === 'string') elt=fdjtID(elt);
	    var c=elt.parentNode; var cc=c.parentNode;
	    // Remove all constraint
	    c.style.height=''; c.style.overflow='visible';
	    // Compute bounds to get height
	    var cstyle=fdjtDOM.getStyle(c);
	    var ccstyle=fdjtDOM.getStyle(cc);
	    var cbounds=
		fdjtDOM.parsePX(cstyle.borderTopWidth)+
		fdjtDOM.parsePX(cstyle.borderBottomWidth)+
		fdjtDOM.parsePX(cstyle.paddingTop)+
		fdjtDOM.parsePX(cstyle.paddingBottom)+
		fdjtDOM.parsePX(cstyle.marginTop)+
		fdjtDOM.parsePX(cstyle.marginBottom);
	    var ccbounds=
		fdjtDOM.parsePX(ccstyle.borderTopWidth)+
		fdjtDOM.parsePX(ccstyle.borderBottomWidth)+
		fdjtDOM.parsePX(ccstyle.paddingTop)+
		fdjtDOM.parsePX(ccstyle.paddingBottom)+
		fdjtDOM.parsePX(ccstyle.marginTop)+
		fdjtDOM.parsePX(ccstyle.marginBottom);
	    if (Codex.scrolldivs) {
		c.style.height=
		    ((cc.offsetHeight-(ccbounds+cbounds))-c.offsetTop)+'px';
	    	c.style.overflow='';}
	    else {
		if ((!(Codex.scrollers))||(!(elt.id))) return;
		if (Codex.Trace.scroll) {
		    fdjtLog("cco=%o ct=%o nh=%o",
			    cc.offsetHeight,c.offsetTop,
			    cc.offsetHeight-c.offsetTop);}
		c.style.height=
		    ((cc.offsetHeight-(ccbounds+cbounds))-c.offsetTop)+'px';
		c.style.overflow='hidden';
		if ((Codex.scrollers[elt.id])&&
		    (Codex.scrollers[elt.id].element===elt))
		    Codex.scrollers[elt.id].refresh();
		else Codex.scrollers[elt.id]=new iScroll(elt);}
	    if (Codex.Trace.scroll) {
		fdjtLog("updateScroller %o %o %o ch=%o h=%o",
			elt,c,cc,cc.offsetHeight-c.offsetTop,elt.offsetHeight);
		fdjtLog("updateScroller e=%o,c=%o,cc=%o",
			fdjtDOM.getStyle(elt).overflow,
			fdjtDOM.getStyle(c).overflow,
			fdjtDOM.getStyle(cc).overflow);
		if ((!(Codex.nativescroll))&&
		    (elt.id)&&(Codex.scrollers)&&
		    (Codex.scrollers[elt.id])) {
		    var scroller=Codex.scrollers[elt.id];
		    fdjtLog("e=%o w=%o wo=%o,%o wc=%o,%o i=%o,%o o=%o,%o d=%o,%o m=%o,%o",
			    scroller.element,scroller.wrapper,
			    scroller.wrapper.offsetWidth,
			    scroller.wrapper.offsetHeight,
			    scroller.wrapper.clientWidth,
			    scroller.wrapper.clientHeight,
			    elt.offsetWidth,elt.offsetHeight,
			    scroller.scrollerWidth,scroller.scrollerHeight,
			    scroller.scrollWidth,scroller.scrollHeight,
			    scroller.maxScrollX,scroller.maxScrollY);}}}
	Codex.UI.updateScroller=updateScroller;

	function CodexHUDToggle(mode,keephud){
	    if (!(Codex.mode)) CodexMode(mode);
	    else if (mode===Codex.mode)
		if (keephud) CodexMode(true); else CodexMode(false);
	    else if ((mode==='flyleaf')&&
		     (Codex.mode.search(codexflyleafMode_pat)===0))
		if (keephud) CodexMode(true); else CodexMode(false);
	    else CodexMode(mode);}
	CodexMode.toggle=CodexHUDToggle;

	Codex.dropHUD=function(){return CodexMode(false);}
	Codex.toggleHUD=function(evt){
	    evt=evt||event;
	    if ((evt)&&(fdjtUI.isClickable(fdjtUI.T(evt)))) return;
	    fdjtLog("toggle HUD %o hudup=%o",evt,Codex.hudup);
	    if (Codex.hudup) setHUD(false,false);
	    else setHUD(true);};
	
	/* The App HUD */
	
	function fillinFlyleaf(){
	    var hidehelp=fdjtID("SBOOKHIDEHELP");
	    var dohidehelp=fdjtState.getCookie("sbookhidehelp");
	    if (!(hidehelp)) {}
	    else if (dohidehelp==='no') hidehelp.checked=false;
	    else if (dohidehelp) hidehelp.checked=true;
	    else hidehelp.checked=false;
	    if (hidehelp)
		hidehelp.onchange=function(evt){
		    if (hidehelp.checked)
			fdjtState.setCookie("sbookhidehelp",true,false,"/");
		    else fdjtState.setCookie("sbookhidehelp","no",false,"/");};
	    var refuris=document.getElementsByName("REFURI");
	    if (refuris) {
		var i=0; var len=refuris.length;
		while (i<len)
		    if (refuris[i].value==='fillin')
			refuris[i++].value=Codex.refuri;
		else i++;}
	    fillinAboutInfo();
	    /* Get various external APPLINK uris */
	    var offlineuri=fdjtDOM.getLink("codex.offline")||altLink("offline");
	    var epuburi=fdjtDOM.getLink("codex.epub")||altLink("ebub");
	    var mobiuri=fdjtDOM.getLink("codex.mobi")||altLink("mobi");
	    var zipuri=fdjtDOM.getLink("codex.mobi")||altLink("mobi");
	    if (offlineuri) {
		var elts=document.getElementsByName("SBOOKOFFLINELINK");
		var i=0; while (i<elts.length) {
		    var elt=elts[i++];
		    if (offlineuri!=='none') elt.href=offlineuri;
		    else {
			elt.href=false;
			addClass(elt,"deadlink");
			elt.title='this sBook is not available offline';}}}
	    if (epuburi) {
		var elts=document.getElementsByName("SBOOKEPUBLINK");
		var i=0; while (i<elts.length) {
		    var elt=elts[i++];
		    if (epuburi!=='none') elt.href=epuburi;
		    else {
			elt.href=false;
			addClass(elt,"deadlink");
			elt.title='this sBook is not available as an ePub';}}}
	    if (mobiuri) {
		var elts=document.getElementsByName("SBOOKMOBILINK");
		var i=0; while (i<elts.length) {
		    var elt=elts[i++];
		    if (mobiuri!=='none') elt.href=mobiuri;
		    else {
			elt.href=false;
			addClass(elt,"deadlink");
			elt.title='this sBook is not available as a MOBIpocket format eBook';}}}
	    if (zipuri) {
		var elts=document.getElementsByName("SBOOKZIPLINK");
		var i=0; while (i<elts.length) {
		    var elt=elts[i++];
		    if (zipuri!=='none') elt.href=zipuri;
		    else {
			elt.href=false;
			addClass(elt,"deadlink");
			elt.title='this sBook is not available as a ZIP bundle';}}}
	    initManageIFrame();
	    /* If the book is offline, don't bother showing the link to the offline
	       version. */
	    if (Codex.offline) addClass(document.body,"sbookoffline");}

	function altLink(type,uri){
	    uri=uri||Codex.refuri;
	    if (uri.search("http://")===0)
		return "http://offline."+uri.slice(7);
	    else if (uri.search("https://")===0)
		return "https://offline."+uri.slice(8);
	    else return false;}

	function _sbookFillTemplate(template,spec,content){
	    if (!(content)) return;
	    var elt=fdjtDOM.$(spec,template);
	    if ((elt)&&(elt.length>0)) elt=elt[0];
	    else return;
	    if (typeof content === 'string')
	      elt.innerHTML=content;
	    else if (content.cloneNode)
		fdjtDOM.replace(elt,content.cloneNode(true));
	    else fdjtDOM(elt,content);}

	function fillinAboutInfo(){
	    var about=fdjtID("APPABOUT");
	    var bookabout=fdjtID("SBOOKABOUTPAGE")||fdjtID("SBOOKABOUT");
	    var authorabout=fdjtID("SBOOKAUTHORPAGE")||fdjtID("SBOOKABOUTAUTHOR");
	    if (bookabout) fdjtDOM(about,bookabout);
	    else {
		var title=
		    fdjtID("SBOOKTITLE")||
		    fdjtDOM.getMeta("codex.title")||
		    fdjtDOM.getMeta("TITLE")||
		    fdjtDOM.getMeta("DC.title")||
		    document.title;
		var byline=
		    fdjtID("SBOOKBYLINE")||fdjtID("SBOOKAUTHOR")||
		    fdjtDOM.getMeta("codex.byline")||fdjtDOM.getMeta("BYLINE")||
		    fdjtDOM.getMeta("codex.author")||fdjtDOM.getMeta("AUTHOR");
		var copyright=
		    fdjtID("SBOOKCOPYRIGHT")||
		    fdjtDOM.getMeta("codex.copyright")||fdjtDOM.getMeta("COPYRIGHT")||
		    fdjtDOM.getMeta("RIGHTS");
		var publisher=
		    fdjtID("SBOOKPUBLISHER")||
		    fdjtDOM.getMeta("codex.publisher")||
		    fdjtDOM.getMeta("PUBLISHER");
		var description=
		    fdjtID("SBOOKDESCRIPTION")||
		    fdjtDOM.getMeta("codex.description")||
		    fdjtDOM.getMeta("DESCRIPTION");
		var digitized=
		    fdjtID("SBOOKDIGITIZED")||
		    fdjtDOM.getMeta("codex.digitized")||
		    fdjtDOM.getMeta("DIGITIZED");
		var sbookified=fdjtID("SBOOKIFIED")||fdjtDOM.getMeta("SBOOKIFIED");
		_sbookFillTemplate(about,".title",title);
		_sbookFillTemplate(about,".byline",byline);
		_sbookFillTemplate(about,".publisher",publisher);
		_sbookFillTemplate(about,".copyright",copyright);
		_sbookFillTemplate(about,".description",description);
		_sbookFillTemplate(about,".digitized",digitized);
		_sbookFillTemplate(about,".sbookified",sbookified);
		_sbookFillTemplate(about,".about",fdjtID("SBOOKABOUT"));
		var cover=fdjtDOM.getLink("cover");
		if (cover) {
		    var cover_elt=fdjtDOM.$(".cover",about)[0];
		    if (cover_elt) fdjtDOM(cover_elt,fdjtDOM.Image(cover));}}
	    if (authorabout) fdjtDOM(about,authorabout);}

	function initManageIFrame(){
	    var query=document.location.search||"?";
	    var refuri=Codex.refuri;
	    var appuri="https://"+Codex.server+"/flyleaf"+query;
	    if (query.search("REFURI=")<0)
		appuri=appuri+"&REFURI="+encodeURIComponent(refuri);
	    if (query.search("TOPURI=")<0)
		appuri=appuri+"&TOPURI="+
		encodeURIComponent(document.location.href);
	    if (document.title) {
		appuri=appuri+"&DOCTITLE="+encodeURIComponent(document.title);}
	    fdjtID("MANAGEAPP").src=appuri;}

	CodexMode.selectApp=function(){
	    /* initManageIFrame(); */
	    if (Codex.mode==='sbookapp') CodexMode(false);
	    else CodexMode('sbookapp');}

	/* Scanning */

	function CodexScan(elt,src){
	    var cxt=false;
	    var body=document.body;
	    var pelt=Codex.scanning;
	    if (Codex.Trace.mode)
		fdjtLog("CodexScan() %o (src=%o) mode=%o scn=%o/%o",
			elt,src,Codex.mode,Codex.scanning,Codex.target);
	    // Save the source HUD element for the preview (when provided)
	    if (Codex.scanning!==src) {
		var clone=src.cloneNode(true);
		clone.id="CODEXSCAN";
		fdjtDOM.replace("CODEXSCAN",clone);
		if (Codex.nextSlice(src))
		    dropClass("CODEXHUD","scanend");
		else addClass("CODEXHUD","scanend");
		if (Codex.prevSlice(src))
		    dropClass("CODEXHUD","scanstart");
		else addClass("CODEXHUD","scanstart");
		Codex.scanning=src;}
	    else {}
	    Codex.setTarget(elt);
	    Codex.GoTo(elt);
	    CodexMode("scanning");}
	Codex.Scan=CodexScan;

	Codex.addConfig("uisize",function(name,value){
	    fdjtDOM.swapClass(CodexHUD,/codexuifont\w+/,"codexuifont"+value);});
	Codex.addConfig("showconsole",function(name,value){
	    if (value) addClass(CodexHUD,"codexshowconsole");
	    else dropClass(CodexHUD,"codexshowconsole");});
	Codex.addConfig("animatepages",function(name,value){
	    if (value) addClass(Codex.page,"codexanimate");
	    else dropClass(Codex.page,"codexanimate");});
	Codex.addConfig("animatehud",function(name,value){
	    if (value) addClass(Codex.HUD,"codexanimate");
	    else dropClass(Codex.HUD,"codexanimate");});

	/* Settings apply/save handlers */

	function getSettings(){
	    var result={};
	    var settings=fdjtID("CODEXSETTINGS");
	    var pageview=fdjtDOM.getInputValues(settings,"CODEXPAGEVIEW");
	    result.pageview=
		((pageview)&&(pageview.length)&&(true))||false;
	    var bodysize=fdjtDOM.getInputValues(settings,"CODEXBODYSIZE");
	    if ((bodysize)&&(bodysize.length))
		result.bodysize=bodysize[0];
	    var bodyfamily=fdjtDOM.getInputValues(settings,"CODEXBODYFAMILY");
	    if ((bodyfamily)&&(bodyfamily.length))
		result.bodyfamily=bodyfamily[0];
	    var uisize=fdjtDOM.getInputValues(settings,"CODEXUISIZE");
	    if ((uisize)&&(uisize.length))
		result.uisize=uisize[0];
	    var hidesplash=fdjtDOM.getInputValues(settings,"CODEXHIDESPLASH");
	    result.hidesplash=
		((hidesplash)&&(hidesplash.length)&&(true))||false;
	    var showconsole=fdjtDOM.getInputValues(settings,"CODEXSHOWCONSOLE");
	    result.showconsole=
		((showconsole)&&(showconsole.length)&&(true))||false;
	    return result;}

	Codex.UI.applySettings=function(){
	  Codex.setConfig(getSettings());};
	Codex.UI.saveSettings=function(){
	  Codex.saveConfig(getSettings());};
	
	/* Console methods */
	function console_eval(){
	    fdjtLog("Executing %s",input_console.value);
	    var result=eval(input_console.value);
	    var string_result=
		((result.nodeType)?
		 (fdjtString("%o",result)):
		 (fdjtString("%j",result)));
	    fdjtLog("Result is %s",string_result);}
	function consolebutton_click(evt){console_eval();}
	function consoleinput_keypress(evt){
	    evt=evt||event;
	    var target=fdjtUI.T(evt);
	    if (evt.keyCode===13) {
		if (!(evt.ctrlKey)) {
		    fdjtUI.cancel(evt);
		    console_eval();
		    if (evt.shiftKey) input_console.value="";}}}

	/* Button methods */

	function LoginButton_ontap(evt){
	    evt=evt||event||null;
	    if (Codex.mode==="sbookapp") CodexMode(false);
	    else CodexMode("sbookapp");
	    evt.cancelBubble=true;}

	return CodexMode;})();

fdjt_versions.decl("codex",codex_hud_version);
fdjt_versions.decl("codex/hud",codex_hud_version);

/* Emacs local variables
   ;;;  Local variables: ***
   ;;;  compile-command: "cd ..; make" ***
   ;;;  End: ***
*/
/* -*- Mode: Javascript; Character-encoding: utf-8; -*- */

var codex_interaction_id="$Id$";
var codex_interaction_version=parseInt("$Revision$".slice(10,-1));

/* Copyright (C) 2009-2011 beingmeta, inc.
   This file implements a Javascript/DHTML UI for reading
   large structured documents (sBooks).

   For more information on sbooks, visit www.sbooks.net
   For more information on knodules, visit www.knodules.net
   For more information about beingmeta, visit www.beingmeta.com

   This library uses the FDJT (www.fdjt.org) toolkit.

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

   Use and redistribution (especially embedding in other
   CC licensed content) is permitted under the terms of the
   Creative Commons "Attribution-NonCommercial" license:

   http://creativecommons.org/licenses/by-nc/3.0/ 

   Other uses may be allowed based on prior agreement with
   beingmeta, inc.  Inquiries can be addressed to:

   licensing@beingmeta.com

   Enjoy!

*/

/* There are three basic display modes:
    reading (minimal decoration, with 'minimal' configurable)
    scanning (card at top, buttons on upper edges)
    tool (lots of tools unfaded)

   Tap on content:
    if not hudup, raise the HUD;
    if scanning or no target, lower the HUD
    if addgloss is live on target, lower the HUD
    otherwise addgloss on target
*/

(function(){

    // Imports (kind of )
    var addClass=fdjtDOM.addClass;
    var hasClass=fdjtDOM.hasClass;
    var dropClass=fdjtDOM.dropClass;
    var swapClass=fdjtDOM.swapClass;
    var toggleClass=fdjtDOM.toggleClass;
    var getTarget=Codex.getTarget;
    var getParent=fdjtDOM.getParent;
    var isClickable=fdjtUI.isClickable;
    var getGeometry=fdjtDOM.getGeometry;

    var submitEvent=fdjtUI.submitEvent;

    var unhold=false;
    var hold_timer=false;
    var hold_interval=1500;
    var start_x=-1; var start_y=-1; var last_x=-1; var last_y=-1;
    var start_t=-1; var last_t=-1;
    function sbicon(base){return Codex.graphics+base;}
    function cxicon(base) {return Codex.graphics+"codex/"+base;}

    /* Setup for gesture handling */

    function addHandlers(node,type){
	var mode=Codex.ui;
	fdjtDOM.addListeners(node,Codex.UI.handlers[mode][type]);}
    Codex.UI.addHandlers=addHandlers;

    function setupGestures(domnode){
	var mode=Codex.ui;
	if (!(mode)) Codex.ui=mode="mouse";
	if (!(domnode)) {
	    addHandlers(false,'window');
	    addHandlers(fdjtID("CODEXPAGE"),'content');
	    addHandlers(Codex.HUD,'hud');}
	var handlers=Codex.UI.handlers[mode];
	if (mode)
	    for (var key in handlers)
		if ((key.indexOf('.')>=0)||(key.indexOf('#')>=0)) {
		    var nodes=fdjtDOM.$(key,domnode);
		    var h=handlers[key];
		    fdjtDOM.addListeners(nodes,h);}}
    Codex.setupGestures=setupGestures;

    var dont=fdjtUI.nobubble;
    function passmultitouch(evt){
	if ((evt.touches)&&(evt.touches.length>1)) return;
	else fdjtUI.nobubble(evt);}

    /* New simpler UI */

    function inUI(node){
	while (node)
	    if (!(node)) return false;
	else if (node.codexui) return true;
	else node=node.parentNode;
	return false;}

    var gloss_focus=false;
    var gloss_blurred=false;
    function addgloss_focus(evt){
	evt=evt||event;
	gloss_blurred=false;
	var target=fdjtUI.T(evt);
	var form=getParent(target,"FORM");
	if (form) addClass(form,"focused");
	gloss_focus=form;}
    function addgloss_blur(evt){
	evt=evt||event;
	var target=fdjtUI.T(evt);
	var form=getParent(target,"FORM");
	if (form) dropClass(form,"focused");
	gloss_blurred=fdjtTime();
	gloss_focus=false;}
    Codex.UI.addgloss_focus=addgloss_focus;
    Codex.UI.addgloss_blur=addgloss_blur;

    /* Adding a gloss button */

    function addGlossButton(target){
	var passage=getTarget(target);
	if (!(passage)) return;
	var img=fdjtDOM.getChild(passage,".codexglossbutton");
	if (img) return;
	img=fdjtDOM.Image(cxicon("remarkballoon32x32.png"),".codexglossbutton",
			  "+","click to add a gloss to this passage");
	Codex.UI.addHandlers(img,"glossbutton");
	fdjtDOM.prepend(passage,img);}
    
    function glossbutton_ontap(evt){
	evt=evt||event;
	var target=fdjtUI.T(evt);
	var passage=getTarget(target);
	if ((Codex.mode==="addgloss")&&
	    (Codex.glosstarget===passage)) {
	    fdjtUI.cancel(evt);
	    CodexMode(true);}
	else if (passage) {
	    fdjtUI.cancel(evt);
	    var form=Codex.setGlossTarget(passage);
	    CodexMode("addgloss");
	    Codex.setGlossForm(form);}}

    var excerpts=[];

    /* New handlers */

    function emptySelection(sel){
	return ((!(sel))||
		(!(sel.focusNode))||
		(!(sel.anchorNode))||
		((sel.anchorNode===sel.focusNode)&&
		 (sel.anchorOffset===sel.focusOffset)));}

    /* Functionality:
       on selection:
       save but keep selection,
       set target (if available)
       if hud is down, raise it
       on tap: (no selection)
       if hud is down, set target and raise it
       if no target, raise hud
       if tapping target, lower HUD
       if tapping other, set target, drop mode, and raise hud
       (simpler) on tap:
       if hudup, drop it
       otherwise, set target and raise HUD
    */

    /* Holding */

    var held=false; var handled=false;

    function clear_hold(caller){
	if (held) {
	    clearTimeout(held); held=false;
	    if (Codex.Trace.gestures)
		fdjtLog("clear_hold from %s",(caller||"somewhere"));}}

    /* Generic content handler */

    function content_tapped(evt,target){
	if (!(target)) target=fdjtUI.T(evt);
	// Don't capture modified events
	if ((evt.shiftKey)||(evt.ctrlKey)||(evt.altKey)) return;
	var anchor=getParent(target,"A"), href;
	// If you tap on a relative anchor, move there using Codex
	// rather than the browser default
	if ((anchor)&&(anchor.href)&&
	    (href=anchor.getAttribute("href"))&&(href[0]==='#')&&
	    (document.getElementById(href.slice(1)))) {
	    var elt=document.getElementById(href.slice(1));
	    // This would be the place to provide smarts for
	    // asides/notes/etc, so they (for example) pop up
	    Codex.JumpTo(elt);
	    fdjtUI.cancel(evt);
	    return;}
	var passage=getTarget(target);
	// We get the passage here so we can include it in the trace message
	if (Codex.Trace.gestures)
	    fdjtLog("content_tapped (%o) on %o passage=%o mode=%o",
		    evt,target,passage,Codex.mode);
	// These should have their own handlers
	if ((isClickable(target))||
	    // (fdjtDOM.hasParent(target,".codexglossbutton"))||
	    (fdjtDOM.hasParent(target,".codexglossmark"))) {
	    if (Codex.Trace.gestures)
		fdjtLog("deferring content_tapped (%o) on %o",
			evt,target,passage,Codex.mode);
	    return;}
	// If there's a selection, store it as an excerpt.
	var sel=window.getSelection();
	if ((sel)&&(sel.anchorNode)&&(!(emptySelection(sel)))) {
	    var p=getTarget(sel.anchorNode)||
		getTarget(sel.focusNode)||
		passage;
	    if (p) {
		if ((Codex.mode==="addgloss")&&
		    (fdjtID("CODEXLIVEGLOSS"))) {
		    Codex.addExcerpt(
			fdjtID("CODEXLIVEGLOSS"),
			sel.toString(),
			((Codex.glosstarget!==p)&&
			 ((p.id)||p.codexdupid)));}
		else {
		    Codex.excerpt=sel.toString();
		    tapTarget(p);}
		return;}
	    else CodexMode(false);}
	if ((Codex.hudup)&&(passage)&&(Codex.mode==='addgloss')&&
	    ((gloss_focus)||((fdjtTime()-gloss_blurred)<1000))) {
	    if (passage===Codex.target) CodexMode(false);
	    else tapTarget(passage);}
	else if ((passage)&&(passage===Codex.target)&&(Codex.hudup)) {
	    Codex.setTarget(false);
	    CodexMode(false);}
	else if (passage)
	    tapTarget(passage);
	else if ((Codex.mode)||(Codex.hudup))
	    CodexMode(false);
	else CodexMode(true);}

    /* Tap actions */

    function tapTarget(target){
	if (Codex.Trace.gestures)
	    fdjtLog("Tap on target %o mode=%o",target,Codex.mode);
	Codex.setTarget(target);
	addGlossButton(target);
	CodexMode(true);}

    function edgeTap(evt,x){
	if (!(evt)) evt=event||false;
	var pageom=getGeometry(Codex.page,document.body);
	if (typeof x !== 'number') x=((evt)&&getOffX(evt));
	if (typeof x !== 'number') x=last_x;
	if (typeof x === 'number') {
	    if (Codex.Trace.gestures)
		fdjtLog("edgeTap %o x=%o w=%o g=%j",evt,x,
			fdjtDOM.viewWidth(),pageom);
	    if (x<0) {Backward(evt); return true;}
	    else if (x>pageom.width) {
		Forward(evt); return true;}
	    else return false}
	else return false;}
    Codex.edgeTap=edgeTap;
    
    function edge_click(evt) {
	var target=fdjtUI.T(evt);
	if ((isClickable(target))||
	    (fdjtDOM.hasParent(target,".codexglossbutton"))||
	    (fdjtDOM.hasParent(target,".codexglossmark")))
	    return;
	if (edgeTap(evt)) fdjtUI.cancel(evt);}

    /* HUD handlers */

    function hud_tapped(evt,target){
	if (!(target)) target=fdjtUI.T(evt);
	if (isClickable(target)) return;
	else if (fdjtDOM.hasParent(target,".helphud")) {
	    var mode=fdjtDOM.findAttrib(target,"data-hudmode")||
		fdjtDOM.findAttrib(target,"hudmode");
	    if (mode) CodexMode(mode)
	    else CodexMode(false);
	    return fdjtUI.cancel(evt);}
	var card=((hasClass(target,"codexcard"))?(target):
		  (getParent(target,".codexcard")));
	if (card) {
	    if ((!(getParent(target,".tool")))&&
		(getParent(card,".codexslice"))) {
		Codex.Scan(fdjtID(card.about),card);
		return fdjtUI.cancel(evt);}
	    else if ((card.name)||(card.getAttribute("name"))) {
		var name=(card.name)||(card.getAttribute("name"));
		var gloss=fdjtKB.ref(name,Codex.glosses);
		if (!(gloss)) return;
		Codex.setGlossTarget(gloss);	    
		CodexMode("addgloss");}
	    else if (card.about) {
		Codex.JumpTo(card.about);}
	    fdjtUI.cancel(evt);
	    return;}
	var scan=target, about=false, frag=false, gloss=false;
	while (scan) {
	    if (about=scan.about) break;
	    else if (frag=scan.frag) break;
	    else scan=scan.parentNode;}
	if (frag) {Codex.ScanTo(frag); fdjtUI.cancel(evt);}
	else if ((about)&&(about[0]==='#')) {
	    Codex.ScanTo(about.slice(0)); fdjtUI.cancel(evt);}
	else if ((about)&&(gloss=Codex.glosses.ref(about))) {
	    Codex.setGlossTarget(gloss);	    
	    CodexMode("addgloss");
	    fdjtUI.cancel(evt);}
	else {}}
    
    /* Mouse handlers */

    /* Keyboard handlers */

    // We use keydown to handle navigation functions and keypress
    //  to handle mode changes
    function onkeydown(evt){
	evt=evt||event||null;
	var kc=evt.keyCode;
	// Codex.trace("sbook_onkeydown",evt);
	if (evt.keyCode===27) { /* Escape works anywhere */
	    if (Codex.mode) {
		Codex.last_mode=Codex.mode;
		CodexMode(false);
		Codex.setTarget(false);
		fdjtID("CODEXSEARCHINPUT").blur();}
	    else if (Codex.last_mode) CodexMode(Codex.last_mode);
	    else {}
	    return;}
	else if ((evt.altKey)||(evt.ctrlKey)||(evt.metaKey)) return true;
	else if (kc===34) Codex.Forward(evt);   /* page down */
	else if (kc===33) Codex.Backward(evt);  /* page up */
	else if (kc===37) Codex.scanBackward(evt); /* arrow left */
	else if (kc===39) Codex.scanForward(evt); /* arrow right */
	// Don't interrupt text input for space, etc
	else if (fdjtDOM.isTextInput(fdjtDOM.T(evt))) return true;
	else if (kc===32) Codex.Forward(evt); // Space
	// backspace or delete
	else if ((kc===8)||(kc===45)) Codex.Backward(evt);
	// Home goes to the current head.
	else if (kc===36) Codex.JumpTo(Codex.head);
	else return;
	fdjtUI.cancel(evt);}

    // At one point, we had the shift key temporarily raise/lower the HUD.
    //  We might do it again, so we keep this definition around
    function onkeyup(evt){
	evt=evt||event||null;
	var kc=evt.keyCode;
	// Codex.trace("sbook_onkeyup",evt);
	if (fdjtDOM.isTextInput(fdjtDOM.T(evt))) return true;
	else if ((evt.ctrlKey)||(evt.altKey)||(evt.metaKey)) return true;
	else {}}
    Codex.UI.handlers.onkeyup=onkeyup;

    /* Keypress handling */

    // We have a big table of command characters which lead to modes
    var modechars={
	63: "searching",102: "searching",
	65: "flyleaf", 97: "flyleaf",
	83: "searching",115: "searching",
	80: "gotopage",112: "gotopage",
	76: "gotoloc",108: "gotoloc",
	70: "searching",
	100: "device",68: "device",
	110: "toc",78: "toc",
	116: "flytoc",84: "flytoc",
	72: "help", 104: "humane",
	103: "allglosses",71: "allglosses",
	67: "console", 99: "console"};

    // Handle mode changes
    function onkeypress(evt){
	var modearg=false; 
	evt=evt||event||null;
	var ch=evt.charCode||evt.keyCode;
	// Codex.trace("sbook_onkeypress",evt);
	if (fdjtDOM.isTextInput(fdjtDOM.T(evt))) return true;
	else if ((evt.altKey)||(evt.ctrlKey)||(evt.metaKey)) return true;
	else modearg=modechars[ch];
	if (modearg==="flyleaf")
	    modearg=Codex.last_flyleaf||"about";
	if (modearg==="humane") {
	    fdjtLog.Humane();
	    return;}
	var mode=CodexMode();
	if (modearg) {
	    if (mode===modearg) {
		CodexMode(false); mode=false;}
	    else {
		CodexMode(modearg); mode=modearg;}}
	else {}
	if (mode==="searching")
	    Codex.setFocus(fdjtID("CODEXSEARCHINPUT"));
	else fdjtID("CODEXSEARCHINPUT").blur();
	fdjtDOM.cancel(evt);}
    Codex.UI.handlers.onkeypress=onkeypress;

    function goto_keypress(evt){
	evt=evt||event||null;
	var target=fdjtUI.T(evt);
	var ch=evt.charCode||evt.keyCode;
	var max=false; var min=false;
	if (target.name==='GOTOLOC') {
	    min=0; max=Math.floor(Codex.ends_at/128);}
	else if (target.name==='GOTOPAGE') {
	    min=1; max=Codex.pagecount;}
	else if (ch===13) fdjtUI.cancel(evt);
	if (ch===13) {
	    var num=parseInt(target.value);
	    fdjtUI.cancel(evt);
	    if ((typeof num !== 'number')||(num<min)||(num>max)) {
		alert("Enter a number between "+min+" and "+max+" (inclusive)");
		return;}
	    if (target.name==='GOTOLOC') Codex.JumpTo(128*num);
	    else if (target.name==='GOTOPAGE') Codex.GoToPage(num);
	    else {}
	    target.value="";
	    CodexMode(false);}}
    Codex.UI.goto_keypress=goto_keypress;

    /* ADDGLOSS interaction */

    function delete_ontap(evt){
	evt=evt||event;
	var target=fdjtUI.T(evt);
	fdjtUI.cancel(evt);
	var block=getParent(target,".codexglossform");
	if (!(block)) return;
	var glosselt=fdjtDOM.getInput(block,'UUID');
	if (!(glosselt)) return;
	var qref=glosselt.value;
	var gloss=Codex.glosses.ref(qref);
	if (!(gloss)) return;
	var frag=gloss.get("frag");
	fdjtAjax.jsonCall(
	    function(response){glossdeleted(response,qref,frag);},
	    "https://"+Codex.server+"/glosses/delete",
	    "gloss",qref);}
    Codex.UI.delete_ontap=delete_ontap;
    
    function respond_ontap(evt){
	evt=evt||event;
	var target=fdjtUI.T(evt);
	fdjtUI.cancel(evt);
	var block=getParent(target,".codexglossform");
	if (!(block)) return;
	var glosselt=fdjtDOM.getInput(block,'UUID');
	if (!(glosselt)) return;
	var qref=glosselt.value;
	var gloss=Codex.glosses.ref(qref);
	if (!(gloss)) return;
	Codex.setGlossTarget(gloss,Codex.getGlossForm(gloss,true));
	CodexMode("addgloss");}
    Codex.UI.respond_ontap=respond_ontap;

    function glossdeleted(response,glossid,frag){
	if (response===glossid) {
	    Codex.glosses.drop(glossid);
	    Codex.allglosses=fdjtKB.remove(Codex.allglosses,glossid);
	    if (Codex.offline)
		fdjtState.setLocal("glosses("+Codex.refuri+")",
				   Codex.allglosses,true);
	    var editform=fdjtID("CODEXEDITGLOSS_"+glossid);
	    if (editform) {
		var editor=editform.parentNode;
		if (editor===fdjtID('CODEXLIVEGLOSS')) {
		    Codex.glosstarget=false;
		    CodexMode(false);}
		fdjtDOM.remove(editor);}
	    var renderings=fdjtDOM.Array(document.getElementsByName(glossid));
	    if (renderings) {
		var i=0; var lim=renderings.length;
		while (i<lim) {
		    var rendering=renderings[i++];
		    if (rendering.id==='CODEXSCAN')
			fdjtDOM.replace(rendering,fdjtDOM("div.codexcard.deletedgloss"));
		    else fdjtDOM.remove(rendering);}}
	    var glossmark=fdjtID("SBOOK_GLOSSMARK_"+frag);
	    if (glossmark) {
		var newglosses=fdjtKB.remove(glossmark.glosses,glossid);
		if (newglosses.length===0) fdjtDOM.remove(glossmark);
		else glossmark.glosses=newglosses;}}
	else alert(response);}

    /* HUD button handling */

    var mode_hud_map={
	"toc": "CODEXTOC",
	"searching": "CODEXSEARCH",
	"allglosses": "CODEXSOURCES",
	"flyleaf": "CODEXFLYHEAD"};
    
    function hudbutton(evt){
	evt=evt||event;
	var target=fdjtUI.T(evt);
	var mode=target.getAttribute("hudmode");
	if ((Codex.Trace.gestures)&&
	    ((evt.type==='click')||(Codex.Trace.gestures>1)))
	    fdjtLog("hudbutton() %o mode=%o cl=%o scan=%o sbh=%o mode=%o",
		    evt,mode,(isClickable(target)),
		    Codex.scanning,Codex.hudup,CodexMode());
	fdjtUI.cancel(evt);
	if (!(mode)) return;
	var hudid=((mode)&&(mode_hud_map[mode]));
	var hud=fdjtID(hudid);
	if ((evt.type==='click')||(evt.type==='touchend')) {
	    if (hud) dropClass(hud,"hover");
	    if (fdjtDOM.hasClass(Codex.HUD,mode)) CodexMode(false);
	    else CodexMode(mode);}
	else if ((evt.type==='mouseover')&&(Codex.mode))
	    return;
	else {
	    if (!(hud)) {}
	    else if (evt.type==='mouseover')
		addClass(hud,"hover");
	    else if (evt.type==='mouseout')
		dropClass(hud,"hover");
	    else {}}}
    Codex.UI.hudbutton=hudbutton;

    Codex.UI.dropHUD=function(evt){
	var target=fdjtUI.T(evt);
	if (isClickable(target)) {
	    if (Codex.Trace.gestures)
		fdjtLog("Clickable: don't dropHUD %o",evt);
	    return;}
	if (Codex.Trace.gestures) fdjtLog("dropHUD %o",evt);
	fdjtUI.cancel(evt); CodexMode(false);};

    /* Gesture state */

    var touch_started=false; var touch_ref=false;
    var page_x=-1; var page_y=-1; var sample_t=-1;
    var touch_moves=0;
    var touch_timer=false;
    var touch_held=false;
    var touch_moved=false;
    var touch_scrolled=false;
    var n_touches=0;

    var doubletap=false, tripletap=false;

    function cleartouch(){
	touch_started=false; touch_ref=false;
	start_x=start_y=last_x=last_y=-1;
	page_x=page_y=sample_t=-1; touch_moves=0;
	touch_timer=false; touch_held=false;
	touch_moved=false; touch_scrolled=false;
	doubletap=false; tripletap=false;}

    function tracetouch(handler,evt){
	evt=evt||event;
	var touches=evt.touches;
	var touch=(((touches)&&(touches.length))?(touches[0]):(evt));
	var target=fdjtUI.T(evt); var ref=Codex.getRef(target);
	if (touch_started)
	    fdjtLog("%s(%o) n=%o %sts=%o %s@%o\n\t+%o %s%s%s%s%s%s%s s=%o,%o l=%o,%o p=%o,%o d=%o,%o ref=%o tt=%o tm=%o",
		    handler,evt,((touches)&&(touches.length)),
		    ((!(touch))?(""):
		     ("c="+touch.clientX+","+touch.clientY+";s="+touch.screenX+","+touch.screenY+" ")),
		    touch_started,evt.type,target,
		    fdjtTime()-touch_started,
		    ((Codex.mode)?(Codex.mode+" "):""),
		    ((Codex.scanning)?"scanning ":""),
		    ((touch_held)?("held "):("")),
		    ((touch_moved)?("moved "):("")),
		    ((touch_scrolled)?("scrolled "):("")),
		    ((isClickable(target))?("clickable "):("")),
		    ((touch)?"":"notouch "),
		    start_x,start_y,last_x,last_y,page_x,page_y,
		    (((touch)&&(touch.screenX))?(touch.screenX-page_x):0),
		    (((touch)&&(touch.screenY))?(touch.screenY-page_y):0),
		    touch_ref,touch_timer,touch_moves);
	else fdjtLog("%s(%o) n=%o %s%s c=%o,%o p=%o,%o ts=%o %s@%o ref=%o",
		     handler,evt,((touches)&&(touches.length)),
		     ((Codex.mode)?(Codex.mode+" "):""),
		     ((Codex.scanning)?"scanning ":""),
		     touch.clientX,touch.clientY,touch.screenX,touch.screenY,
		     touch_started,evt.type,target,ref);
	if (ref) fdjtLog("%s(%o) ref=%o from %o",handler,evt,ref,target);}

    /* Touch handling */

    function shared_touchstart(evt){
	evt=evt||event||false;
	var target=fdjtUI.T(evt);
	if (isClickable(target)) return;
	// fdjtUI.cancel(evt);
	if (Codex.Trace.gestures) tracetouch("touchstart",evt);
	touch_started=fdjtTime();
	var touches=evt.touches;
	var touch=(((touches)&&(touches.length))?(touches[0]):(evt));
	if (touches) n_touches=touches.length;
	else if (evt.shiftKey) n_touches=2;
	else n_touches=1;
	if (touch) {
	    start_t=fdjtTime();
	    start_x=last_x=touch.clientX;
	    start_y=last_y=touch.clientY;
	    page_x=touch.screenX; page_y=touch.screenY;}
	else if (evt.clientX) { /* faketouch */
	    if (evt.shiftKey) n_touches=2; else n_touches=1;
	    start_t=fdjtTime();
	    start_x=last_x=evt.clientX;
	    start_y=last_y=evt.clientY;
	    page_x=touch.screenX; page_y=evt.screenY;}
	touch_held=false; touch_moved=false; touch_scrolled=false;}

    var initial_offset=false;

    function content_touchstart(evt){
	evt=evt||event||false;
	clear_hold("touchstart/touchover");
	handled=false;
	var target=fdjtUI.T(evt);
	shared_touchstart(evt);
	var passage=getTarget(target);
	if (Codex.Trace.gestures)
	    fdjtLog("Touchstart %o on %o => %o",evt,target,passage);
	if (passage) {
	    var text=fdjtDOM.textify(passage).
		replace(/\n\n+/g,"\n").
		replace(/^\n+/,"").
		replace(/\n+$/,"").
		replace(/\n+/g," // ");
	    held=setTimeout(function(){
		clear_hold("completed");
		handled=true;
		Codex.setGlossTarget(passage);
		fdjtID("CODEXEXTRACT").passageid=
		    (passage.id||(passage.codexdupid));
		fdjtID("CODEXEXTRACT").value=text;
		CodexMode("editexcerpt");},
			    1000);}
	var translation=Codex.pages.style.getPropertyValue(fdjtDOM.transform);
	var numstart; var numend;
	initial_offset=false;
	if (translation) {
	    var numstart=translation.search(/[\-0123456789]+/);
	    if (numstart>0) {
		translation=translation.slice(numstart);
		var numend=translation.search(/[^\-0123456789]+/);
		if (numend>0)
		    initial_offset=parseInt(translation.slice(0,numend));}}}
    Codex.UI.useExcerpt=function(flag){
	var text=fdjtID("CODEXEXTRACT").value;
	var excerpt_elt=fdjtID("CODEXEXCERPT");
	var form=fdjtID("CODEXLIVEGLOSS");
	if (flag) {
	    Codex.addExcerpt(form,text,excerpt_elt.passageid);
	    CodexMode("addgloss");}
	else CodexMode("false");};
    
    var mouseisdown=false;

    function content_touchmove(evt){
	// When faking touch, moves only get counted if the mouse is down.
	if ((evt.type==="mousemove")&&(!(mouseisdown))) return;
	// fdjtUI.cancel(evt);
	touch_moves++;
	clear_hold("touchmove");
	var touches=evt.touches;
	var touch=
	    (((evt.touches)&&(evt.touches.length))?(evt.touches[0]):(evt));
	if (page_x<0) page_x=touch.screenX;
	if (page_y<0) page_y=touch.screenY;
	if ((touches)&&(touches.length>n_touches)) n_touches=touches.length;
	var dx=touch.screenX-page_x; var dy=touch.screenY-page_y;
	var adx=((dx<0)?(-dx):(dx)); var ady=((dy<0)?(-dy):(dy));
	if (Codex.Trace.gestures>2) tracetouch("touchmove",evt);
	/*
	  if ((held)&&((adx+ady)>5)) {
	  clear_hold("touchmove"+(adx+ady)); handled=true;}
	*/
	if (Codex.Trace.gestures>1)
	    fdjtLog("body_touchmove d=%o,%o a=%o,%o s=%o,%o c=%o,%o l=%o,%o n=%o scan=%o ",
		    dx,dy,adx,ady,touch.screenX,touch.screenY,
		    touch.clientX,touch.clientY,last_x,last_y,
		    touch_moves,Codex.scanning);
	last_x=touch.clientX; last_y=touch.clientY;
	touch_moved=true;
	/*
	// This provides direct interaction but looks a little clunky
	if (typeof initial_offset === 'number') {
	var new_translation="translate("+(initial_offset+dx)+"px,0px)";
	Codex.pages.style.setProperty
	(fdjtDOM.transform,new_translation,"important");}
	*/
	return;}
    
    function content_touchend(evt,tap){
	var target=fdjtUI.T(evt);
	if (held) clear_hold("touchend");
	if (handled) return;
	if (Codex.Trace.gestures) tracetouch("touchend",evt);
	mouseisdown=false; // For faketouch
	if (isClickable(target)) return;
	if (touch_moved) {
	    var dx=last_x-start_x; var dy=last_y-start_y;
	    var adx=((dx<0)?(-dx):(dx)); var ady=((dy<0)?(-dy):(dy));
	    var ad=((adx<ady)?(ady-adx):(adx-ady));
	    if (Codex.Trace.gestures)
		fdjtLog("touchend/gesture l=%o,%o s=%o,%o d=%o,%o |d|=%o,%o",
			last_x,last_y,start_x,start_y,dx,dy,adx,ady);
	    if (adx>(ady*3)) { /* horizontal */
		if (n_touches===1) {
		    if (dx<0) Codex.Forward(evt);
		    else Codex.Backward(evt);}
		else {
		    if (dx<0) Codex.scanForward(evt);
		    else Codex.scanBackward(evt);}}
	    else {}
	    return;}
	else if (touch_scrolled) return;  // Gesture already intepreted
	else return content_tapped(evt,target);}

    /* HUD touch */

    function hud_touchmove(evt){
	// When faking touch, moves only get counted if the mouse is down.
	if ((evt.type==="mousemove")&&(!(mouseisdown))) return;
	var target=fdjtUI.T(evt);
	if (isClickable(target)) return;
	fdjtUI.cancel(evt);
	touch_moves++;
	var touch=
	    (((evt.touches)&&(evt.touches.length))?(evt.touches[0]):(evt));
	var dx=touch.screenX-page_x; var dy=touch.screenY-page_y;
	var adx=((dx<0)?(-dx):(dx)); var ady=((dy<0)?(-dy):(dy));
	if (page_x<0) page_x=touch.screenX;
	if (page_y<0) page_y=touch.screenY;
	if (Codex.Trace.gestures>1) tracetouch("hud_touchmove",evt);
	if ((hold_timer)&&((adx+ady)>4)) {
	    clearTimeout(hold_timer); hold_timer=false;}
	last_x=touch.clientX; last_y=touch.clientY;
	touch_moved=true;
	page_x=touch.screenX; page_y=touch.screenY;
	touch_scrolled=true;}

    function hud_touchend(evt){
	if (Codex.Trace.gestures) tracetouch("hud_touchend",evt);
	var target=fdjtUI.T(evt);
	mouseisdown=false; // For faketouch
	var scroller=((Codex.scrolling)&&(Codex.scrollers)&&
		      (Codex.scrollers[Codex.scrolling]));
	if ((scroller)&&(scroller.motion)&&(scroller.motion>10)) return;
	else if (isClickable(target)) {
	    if (Codex.ui==="faketouch") {
		// This happens automatically when faking touch
		fdjtUI.cancel(evt);
		return;}
	    else {
		var click_evt = document.createEvent("MouseEvents");
		while (target)
		    if (target.nodeType===1) break;
		else target=target.parentNode;
		if (!(target)) return;
		if (Codex.Trace.gestures)
		    fdjtLog("Synthesizing click on %o",target);
		click_evt.initMouseEvent("click", true, true, window,
					 1,page_x,page_y,last_x, last_y,
					 false, false, false, false, 0, null);
		fdjtUI.cancel(evt);
		target.dispatchEvent(click_evt);
		return;}}
	else return hud_tapped(evt);}

    /* Glossmarks */
    
    function glossmark_tapped(evt){
	evt=evt||event||null;
	if (held) clear_hold("glossmark_tapped");
	var target=fdjtUI.T(evt);
	var glossmark=getParent(target,".codexglossmark");
	var passage=getTarget(glossmark.parentNode,true);
	if (Codex.Trace.gestures)
	    fdjtLog("glossmark_tapped (%o) on %o gmark=%o passage=%o mode=%o target=%o",
		    evt,target,glossmark,passage,Codex.mode,Codex.target);
	if (!(glossmark)) return false;
	fdjtUI.cancel(evt);
	if ((Codex.mode==='glosses')&&(Codex.target===passage)) {
	    CodexMode(true);
	    return;}
	else Codex.showGlosses(passage);}

    /* Moving forward and backward */

    var last_motion=false;

    function Forward(evt){
	var now=fdjtTime();
	if (!(evt)) evt=event||false;
	if (evt) fdjtUI.cancel(evt);
	if ((last_motion)&&((now-last_motion)<100)) return;
	else last_motion=now;
	if (Codex.Trace.nav)
	    fdjtLog("Forward e=%o h=%o t=%o",evt,Codex.head,Codex.target);
	if ((Codex.mode==="glosses")||(Codex.mode==="addgloss"))
	    CodexMode(true);
	if (((evt)&&(evt.shiftKey))||(n_touches>1))
	    scanForward();
	else pageForward();}
    Codex.Forward=Forward;
    function right_margin(evt){
	if (Codex.Trace.gestures) tracetouch("right_margin",evt);
	if (Codex.hudup) CodexMode(false);
	else Forward(evt);}

    function Backward(evt){
	var now=fdjtTime();
	if (!(evt)) evt=event||false;
	if (evt) fdjtUI.cancel(evt);
	if ((last_motion)&&((now-last_motion)<100)) return;
	else last_motion=now;
	if ((Codex.mode==="glosses")||(Codex.mode==="addgloss"))
	    CodexMode(true);
	if (Codex.Trace.nav)
	    fdjtLog("Backward e=%o h=%o t=%o",evt,Codex.head,Codex.target);
	if (((evt)&&(evt.shiftKey))||(n_touches>1))
	    scanBackward();
	else pageBackward();}
    Codex.Backward=Backward;
    function left_margin(evt){
	if (Codex.Trace.gestures) tracetouch("left_margin",evt);
	if (Codex.hudup) CodexMode(false);
	else Backward(evt);}


    function pageForward(){
	if (Codex.Trace.gestures)
	    fdjtLog("pageForward c=%o n=%o",Codex.curpage,Codex.pagecount);
	if ((Codex.mode==="scanning")||(Codex.mode==="tocscan"))
	    CodexMode(false);
	if ((Codex.paginate)&&(Codex.colbreak)&&(Codex.pages)) {
	    if (Codex.curpage===Codex.pagecount) {}
	    else Codex.GoToPage(Codex.curpage=(Codex.curpage+1));}
	else if ((Codex.paginate)&&(Codex.pagecount)) {
	    var newpage=false;
	    if (Codex.mode==="glosses") CodexMode(true);
	    if (Codex.curpage===Codex.pagecount) {}
	    else Codex.GoToPage(newpage=Codex.curpage+1);
	    if ((false)&&(newpage)&&(Codex.mode==='allglosses')) /* to fix */
		Codex.UI.scrollGlosses(
		    Codex.pageinfo[newpage].first,
		    fdjtID("CODEXALLGLOSSES"),true);}
	else {
	    var delta=fdjtDOM.viewHeight()-50;
	    if (delta<0) delta=fdjtDOM.viewHeight();
	    var newy=fdjtDOM.viewTop()+delta;
	    window.scrollTo(fdjtDOM.viewLeft(),newy);}}
    Codex.pageForward=pageForward;

    function pageBackward(){
	if (Codex.Trace.gestures)
	    fdjtLog("pageBackward c=%o n=%o",Codex.curpage,Codex.pagecount);
	if ((Codex.mode==="scanning")||(Codex.mode==="tocscan"))
	    CodexMode(false);
	if ((Codex.paginate)&&(Codex.colbreak)&&(Codex.pages)) {
	    if (Codex.curpage===0) {}
	    else Codex.GoToPage(Codex.curpage=(Codex.curpage-1));}
	else if ((Codex.paginate)&&(Codex.pagecount)) {
	    var newpage=false;
	    if (Codex.mode==="glosses") CodexMode(true);
	    if (Codex.curpage===0) {}
	    else {
		Codex.GoToPage(newpage=Codex.curpage-1);}
	    if ((false)&&(newpage)&&(Codex.mode==='allglosses')) /* to fix */
		Codex.UI.scrollGlosses(
		    Codex.pageinfo[newpage].first,
		    fdjtID("CODEXALLGLOSSES"),true);}
	else {
	    var delta=fdjtDOM.viewHeight()-50;
	    if (delta<0) delta=fdjtDOM.viewHeight();
	    var newy=fdjtDOM.viewTop()-delta;
	    window.scrollTo(fdjtDOM.viewLeft(),newy);}}
    Codex.pageBackward=pageBackward;

    function scanForward(){
	if (Codex.mode==="scanning") {}
	else if (Codex.mode==="tocscan") {}
	else if (Codex.scanning) CodexMode("scanning");
	else CodexMode("tocscan");
	if (Codex.mode==="tocscan") {
	    var head=Codex.head;
	    var headid=head.id||head.codexdupid;
	    var headinfo=Codex.docinfo[headid];
	    if (Codex.Trace.nav) 
		fdjtLog("scanForward/toc() head=%o info=%o n=%o h=%o",
			head,headinfo,headinfo.next,headinfo.head);
	    if (headinfo.next) Codex.GoTo(headinfo.next.elt);
	    else if ((headinfo.head)&&(headinfo.head.next)) {
		Codex.GoTo(headinfo.head.next.elt); CodexMode("toc");}
	    else if ((headinfo.head)&&(headinfo.head.head)&&
		     (headinfo.head.head.next)) 
		Codex.GoTo(headinfo.head.head.next.elt);
	    else CodexMode(false);
	    return;}
	var start=Codex.scanning;
	var scan=Codex.nextSlice(start);
	var ref=((scan)&&(Codex.getRef(scan)));
	if (Codex.Trace.nav) 
	    fdjtLog("scanForward() from %o/%o to %o/%o under %o",
		    start,Codex.getRef(start),scan,ref,slice);
	if ((ref)&&(scan)) Codex.Scan(ref,scan);
	return scan;}
    Codex.scanForward=scanForward;

    function scanBackward(){
	if (Codex.mode==="scanning") {}
	else if (Codex.mode==="tocscan") {}
	else if (Codex.scanning) CodexMode("scanning");
	else CodexMode("tocscan");
	if (Codex.mode==="tocscan") {
	    var head=Codex.head;
	    var headid=head.id||head.codexdupid;
	    var headinfo=Codex.docinfo[headid];
	    if (Codex.Trace.nav) 
		fdjtLog("scanBackward/toc() head=%o info=%o p=%o h=%o",
			head,headinfo,headinfo.prev,headinfo.head);
	    if (headinfo.prev) Codex.GoTo(headinfo.prev.elt);
	    else if (headinfo.head) 
		Codex.GoTo(headinfo.head.elt);
	    else CodexMode(false);
	    return;}
	var scan=Codex.prevSlice(Codex.scanning);
	var ref=((scan)&&(Codex.getRef(scan)));
	if (Codex.Trace.nav) 
	    fdjtLog("scanBackward() from %o/%o to %o/%o under %o",
		    start,Codex.getRef(start),scan,ref,slice);
	if ((ref)&&(scan)) Codex.Scan(ref,scan);
	return scan;}
    Codex.scanBackward=scanBackward;

    function scanner_tapped(evt){
	evt=evt||event;
	var target=fdjtUI.T(evt);
	if (isClickable(target)) return;
	if (getParent(target,".tool")) return;
	var scanning=Codex.scanning;
	if (!(scanning)) return;
	var hudparent=getParent(scanning,".hudpanel");
	if (getParent(scanning,fdjtID("CODEXBROWSEGLOSSES"))) {
	    CodexMode("allglosses");
	    fdjtUI.cancel(evt);}
	else if (getParent(scanning,fdjtID("CODEXSEARCH"))) {
	    CodexMode("searchresults");
	    fdjtUI.cancel(evt);}
	else {}}

    /* Entering page numbers and locations */

    function enterPageNum(evt) {
	evt=evt||event;
	fdjtUI.cancel(evt);
	if (Codex.hudup) {CodexMode(false); return;}
	CodexMode.toggle("gotopage");}
    function enterLocation(evt) {
	evt=evt||event;
	fdjtUI.cancel(evt);
	if (Codex.hudup) {CodexMode(false); return;}
	CodexMode.toggle("gotoloc");}
    
    /* Other handlers */

    function flyleaf_tap(evt){
	if (isClickable(evt)) return;
	else CodexMode(false);}

    function getOffX(evt){
	if (typeof evt.offsetX === "number") return evt.offsetX;
	else if ((evt.touches)&&(evt.touches.length)) {
	    var touch=evt.touches[0];
	    var pinfo=fdjtID("CODEXPAGEINFO");
	    var target=touch.target;
	    while ((target)&&(target.nodeType!==1)) target=target.parentNode;
	    var geom=getGeometry(target,pinfo);
	    var tx=geom.left;
	    return touch.clientX-(tx+pinfo.offsetLeft);}
	else return false;}

    function head_click(evt){
	if (Codex.Trace.gestures) fdjtLog("head_click %o",evt);
	if (isClickable(evt)) return;
	else if (Codex.mode==='help') {
	    fdjtUI.cancel(evt);
	    CodexMode(true);}
	else if (Codex.mode) return;
	else {
	    fdjtUI.cancel(evt);
	    CodexMode(true);}}
    function foot_click(evt){
	if (Codex.Trace.gestures) fdjtLog("foot_click %o",evt);
	if (isClickable(evt)) return;
	else if (Codex.mode) {
	    fdjtUI.cancel(evt);
	    CodexMode(false);
	    return;}}

    function pageinfo_click(evt){
	var pageinfo=fdjtID("CODEXPAGEINFO");
	if ((Codex.hudup)||(Codex.mode)) {
	    fdjtUI.cancel(evt);
	    CodexMode(false);
	    return;}
	var offx=getOffX(evt);
	var offwidth=pageinfo.offsetWidth;
	var gopage=Math.floor((offx/offwidth)*Codex.pagecount)+1;
	if ((Codex.Trace.gestures)||(hasClass(pageinfo,"codextrace")))
	    fdjtLog("pageinfo_click %o off=%o/%o=%o gopage=%o/%o",
		    evt,offx,offwidth,offx/offwidth,
		    gopage,Codex.pagecount);
	if (!(offx)) return;
	fdjtUI.cancel(evt);
	Codex.GoToPage(gopage);
	if ((Codex.mode==="gotoloc")||(Codex.mode==="gotopage"))
	    CodexMode(false);}

    function pageinfo_hover(evt){
	var pageinfo=fdjtID("CODEXPAGEINFO");
	var offx=evt.offsetX;
	if (!(offx)) return;
	var offwidth=pageinfo.offsetWidth;
	var showpage=Math.floor((offx/offwidth)*Codex.pagecount)+1;
	pageinfo.title=fdjtString("%d",showpage);}
    /* This doesn't quite work on the iPad, so we're not currently
       using it. */
    function pageinfo_move(evt){
	var pageinfo=fdjtID("CODEXPAGEINFO"); var offx;
	if (evt.offsetX) {
	    var tx=fdjtDOM.getGeometry(fdjtUI.T(evt),pageinfo).left;
	    offx=evt.offsetX+tx;}
	else offx=getOffX(evt);
	var offwidth=fdjtID("CODEXPAGEINFO").offsetWidth;
	var goloc=Math.floor((offx/offwidth)*Codex.ends_at);
	var page=((Codex.paginate)&&Codex.getPageAt(goloc));
	fdjtUI.cancel(evt);
	/* 
	fdjtLog("%o type=%o ox=%o ow=%o gl=%o p=%o",
		evt,evt.type,offx,offwidth,goloc,page); */
	if ((evt.type==='touchmove')||
	    ((evt.type==='mousemove')&&((evt.button)||(evt.shiftKey)))) {
	    if ((typeof page === 'number')&&(page!==Codex.curpage))
		Codex.GoToPage(page);}}
    

    /* Gloss form handlers */

    /**** Clicking on outlets *****/
    function glossform_outlets_tapped(evt){
	evt=evt||event;
	var target=fdjtUI.T(evt);
	if (getParent(target,".checkspan"))
	    return fdjtUI.CheckSpan.onclick(evt);
	else if (getParent(target,".sharing"))
	    toggleClass(getParent(target,".sharing"),"expanded");
	else {}}
    Codex.UI.outlets_tapped=glossform_outlets_tapped;

    var glossmodes=Codex.glossmodes;

    function glossmode_button(evt){
	evt=evt||event;
	var target=fdjtUI.T(evt);
	var alt=target.alt, altclass, input;
	var form=fdjtDOM.getParent(target,'form');
	if (!(alt)) return;
	if (alt==="tag") {
	    altclass="addtag";
	    input=fdjtDOM.getInput(form,'TAG');}
	else if (alt==="link") {
	    altclass="addlink";
	    input=fdjtDOM.getInput(form,'LINK');}
	else if (alt==="excerpt") {
	    altclass="excerpt";
	    input=fdjtDOM.getInput(form,'EXCERPT');}
	else if (alt==="note") {
	    altclass="editnote";
	    input=fdjtDOM.getInput(form,'NOTE');}
	else return;
	if (alt==="tag") addClass("CODEXADDGLOSS","tagging");
	else dropClass("CODEXADDGLOSS","tagging");
	fdjtLog("glossmode_button gm=%s input=%o",altclass,input);
	if (!(hasClass(form,altclass))) {
	    swapClass(form,glossmodes,altclass);
	    Codex.setFocus(input);}
	else {
	    dropClass(form,glossmodes);
	    if ((alt==="tag")||(alt==="link")||(alt==="excerpt")) {}
	    else {}}}

    /* Rules */

    var nobubble=fdjtUI.nobubble;
    var cancel=fdjtUI.cancel;

    function hideSplashToggle(evt) {
	evt=evt||event;
	var target=fdjtUI.T(evt);
	var newval=(!(Codex.hidesplash));
	var input=getParent(target,"input");
	if (input)
	    setTimeout(function(){
		Codex.setConfig('hidesplash',input.checked);
		Codex.saveConfig();},
		       100);
	else {
	    Codex.setConfig('hidesplash',newval);
	    Codex.saveConfig();}
	if ((newval)&&(Codex._setup)&&
	    ((fdjtTime()-(Codex._setup.getTime()))<30000))
	    CodexMode(false);}
    Codex.UI.handlers.mouse=
	{window: {
	    keyup: onkeyup,
	    keydown: onkeydown,
	    keypress: onkeypress},
	 content: {mouseup: content_tapped},
	 hud: {click: hud_tapped},
	 glossmark: {mouseup: glossmark_tapped},
	 glossbutton: {mouseup: glossbutton_ontap,mousedown: cancel},
	 // ".codexmargin": {click: edge_click},
	 "#CODEXHELP": {click: Codex.UI.dropHUD},
	 "#CODEXFLYLEAF": {click: flyleaf_tap},
	 "#CODEXPAGEINFO": {click: pageinfo_click, mousemove: pageinfo_hover},
	 "#CODEXPAGENOTEXT": {click: enterPageNum},
	 "#CODEXLOCOFF": {click: enterLocation},
	 "#CODEXSCANNER": {click: scanner_tapped},
	 "#CODEXPAGEHEAD": {click: head_click},
	 "#CODEXHEAD": {click: head_click},
	 "#CODEXPAGEFOOT": {click: foot_click},
	 "#CODEXPAGELEFT": {click: left_margin},
	 "#CODEXPAGERIGHT": {click: right_margin},
	 "#HIDESPLASHCHECKSPAN" : {click: hideSplashToggle},
	 "#HIDEHELPBUTTON" : {click: function(evt){CodexMode(false);}},
	 // Not really used any more
	 "#CODEXPAGENEXT": {click: Codex.Forward},
	 /* ".hudbutton": {mouseover:hudbutton,mouseout:hudbutton}, */
	 ".hudmodebutton": {click:hudbutton,mouseup:cancel,mousedown:cancel},
	 toc: {mouseover: fdjtUI.CoHi.onmouseover,
	       mouseout: fdjtUI.CoHi.onmouseout},
	 // GLOSSFORM rules
	 "span.codexglossdelete": { click: delete_ontap },
	 "span.codexglossrespond": { click: respond_ontap },
	 "div.submitbutton": {click: submitEvent },
	 "div.glossetc span.links": {click: fdjtUI.CheckSpan.onclick},
	 "div.glossetc span.tags": {click: fdjtUI.CheckSpan.onclick},
	 "div.glossetc div.sharing": {
	     click: glossform_outlets_tapped},
	 "div.glossetc span.modebuttons": {
	     click: glossmode_button}};

    Codex.UI.handlers.webtouch=
	{window: {keyup:onkeyup,keydown:onkeydown,keypress:onkeypress,
		  touchstart: cancel, touchmove: cancel, touchend: cancel},
	 content: {touchstart: content_touchstart,
		   touchmove: content_touchmove,
		   touchend: content_touchend},
	 hud: {touchstart: shared_touchstart,
	       touchmove: hud_touchmove,
	       touchend: hud_touchend},
	 "#CODEXPAGEHEAD": {
	     touchstart: cancel,
	     touchmove: cancel,
	     touchend: head_click},
	 "#CODEXPAGEFOOT": {
	     touchstart: cancel,
	     touchmove: cancel,
	     touchend: foot_click},
	 "#CODEXPAGELEFT": {
	     touchstart: cancel,
	     touchmove: cancel,
	     touchend: left_margin},
	 "#CODEXPAGERIGHT": {
	     touchstart: cancel,
	     touchmove: cancel,
	     touchend: right_margin},
	 "#CODEXHELP": {touchstart: Codex.UI.dropHUD,
			touchmove: cancel,
			touchend: cancel},
	 "#CODEXSCANNER": {touchstart: scanner_tapped},
	 // "#CODEXFLYLEAF": {touchend: flyleaf_tap},
	 "#CODEXPAGEINFO": {touchstart: pageinfo_click,
			    touchmove: cancel,touchend: cancel},
	 "#CODEXPAGENOTEXT": {touchstart: enterPageNum,
			      touchmove: cancel,touchend: cancel},
	 "#CODEXLOCOFF": {touchstart: enterLocation,
			  touchmove: cancel,touchend: cancel},
	 // Not really used any more
	 "#CODEXPAGENEXT": {touchstart: Codex.Forward,touchmove: cancel, touchend: cancel},
	 ".hudbutton": {touchstart: dont,touchmove: dont, touchend: dont},
	 "#CODEXTABS": {touchstart: dont,touchmove: dont, touchend: dont},
	 "#HIDESPLASHCHECKSPAN" : {touchstart: hideSplashToggle,
				   touchmove: cancel,
				   touchend: cancel},
	 "#HIDEHELPBUTTON" : {click: function(evt){CodexMode(false);},
			      touchmove: cancel,
			      touchend: cancel},
	 ".hudmodebutton": {touchend:hudbutton,
			    touchdown:cancel,
			    touchmove:cancel},
	 glossmark: {touchend: glossmark_tapped,
		     touchstart: cancel,
		     touchmove: cancel},
	 glossbutton: {touchend: glossbutton_ontap,
		       touchstart: cancel,
		       touchmove: cancel},
	 // GLOSSFORM rules
	 "span.codexglossdelete": {
	     touchend: delete_ontap, touchstart: cancel, touchmove: cancel},
	 "span.codexglossrespond": {
	     touchend: respond_ontap, touchstart: cancel, touchmove: cancel},
	 "div.submitbutton": {
	     touchend: submitEvent, touchstart: cancel, touchmove: cancel},
	 "div.glossetc span.links": {
	     touchend: fdjtUI.CheckSpan.onclick,
	     touchstart: cancel, touchmove: cancel},
	 "div.glossetc span.tags": {
	     touchend: fdjtUI.CheckSpan.onclick,
	     touchstart: cancel, touchmove: cancel},
	 "div.glossetc div.sharing": {
	     touchend: glossform_outlets_tapped,
	     touchstart: cancel, touchmove: cancel},
	 "div.glossetc span.modebuttons": {
	     touchend: glossmode_button,
	     touchstart: cancel, touchmove: cancel}};
    
})();

fdjt_versions.decl("codex",codex_interaction_version);
fdjt_versions.decl("codex/interaction",codex_interaction_version);

/* Emacs local variables
   ;;;  Local variables: ***
   ;;;  compile-command: "cd ..; make" ***
   ;;;  End: ***
*/

/* -*- Mode: Javascript; Character-encoding: utf-8; -*- */

var codex_toc_id="$Id$";
var codex_toc_version=parseInt("$Revision$".slice(10,-1));

/* Copyright (C) 2009-2011 beingmeta, inc.
   This file implements a Javascript/DHTML UI for reading
   large structured documents (sBooks).

   For more information on sbooks, visit www.sbooks.net
   For more information on knodules, visit www.knodules.net
   For more information about beingmeta, visit www.beingmeta.com

   This library uses the FDJT (www.fdjt.org) toolkit.

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

   Use and redistribution (especially embedding in other
   CC licensed content) is permitted under the terms of the
   Creative Commons "Attribution-NonCommercial" license:

   http://creativecommons.org/licenses/by-nc/3.0/ 

   Other uses may be allowed based on prior agreement with
   beingmeta, inc.  Inquiries can be addressed to:

   licensing@beingmeta.com

   Enjoy!

*/

/* New NAV hud design
   One big DIV for the whole TOC, use CSS to change what's visible
   General structure:
   div.codextoc.toc0
   div.head (contains section name)
   div.spanbar (contains spanbar)
   div.sub (contains all the subsections names)
   div.codextoc.toc1 (tree for first section)
   div.codextoc.toc1 (tree for second section)
   Controlling display:
   .cur class on current head
   .live class on div.codextoc and parents
*/

/* Building the DIV */

var CodexTOC=
    (function(){
	function sbicon(base){return Codex.graphics+base;}
	function cxicon(base){return Codex.graphics+"codex/"+base;}
	function navicon(kind){
	    if (Codex.touch) {
		switch (kind) {
		case 'right': return cxicon("GoldRightTriangle32.png");
		case 'left': return cxicon("GoldLeftTriangle32.png");
		case 'start': return cxicon("GoldLeftStop32.png");
		case 'end': return cxicon("GoldRightStop32.png");}}
	    else {
		switch (kind) {
		case 'right': return cxicon("GoldRightTriangle24.png");
		case 'left': return cxicon("GoldLeftTriangle24.png");
		case 'start': return cxicon("GoldLeftStop24.png");
		case 'end': return cxicon("GoldRightStop24.png");}}}
	Codex.navicon=navicon;

	function CodexTOC(headinfo,depth,tocspec,prefix,headless){
	    var progressbar=fdjtDOM("HR.progressbar");
	    var head=((headless)?(false):
		      (fdjtDOM("A.sectname",headinfo.title)));
	    var spec=tocspec||"DIV.codextoc";
	    var next_button=
		((head)&&
		 ((headinfo.next)?
		  (fdjtDOM.Image(navicon("right"),false,"next")):
		  (fdjtDOM.Image(navicon("end"),false,"nextstop"))));
	    if ((next_button)&&(headinfo.next))
		next_button.frag=headinfo.next.frag;
	    var back_button=
		((head)&&
		 ((headinfo.prev)?
		  (fdjtDOM.Image(navicon("left"),false,"back")):
		  (fdjtDOM.Image(navicon("start"),false,"backstop"))));
	    if ((back_button)&&(headinfo.prev))
		back_button.frag=headinfo.prev.frag;
	    var toc=fdjtDOM(spec,
			    next_button,back_button,
			    ((head)&&(fdjtDOM("DIV.head",progressbar,head))),
			    generate_spanbar(headinfo),
			    generate_subsections(headinfo));
	    var sub=headinfo.sub;
	    if (!(depth)) depth=0;
	    if (head) {
		head.name="SBR"+headinfo.frag;
		head.frag=headinfo.frag;}
	    toc.sbook_start=headinfo.starts_at;
	    toc.sbook_end=headinfo.ends_at;
	    fdjtDOM.addClass(toc,"toc"+depth);
	    toc.id=(prefix||"CODEXTOC4")+headinfo.frag;
	    if ((!(sub))||(!(sub.length))) {
		fdjtDOM.addClass(toc,"codextocleaf");
		return toc;}
	    var i=0; var n=sub.length;
	    while (i<n) {
		toc.appendChild(CodexTOC(sub[i++],depth+1,spec,prefix,headless));}
	    return toc;}
	
	function tocJump(evt,target){
	    if (!(target)) target=fdjtUI.T(evt);
	    while (target) {
		if (target.frag) break;
		else target=target.parentNode;}
	    if (target) {
		var info=Codex.docinfo[target.frag];
		Codex.GoTo(target.frag);
		// if ((info.sub)&&(info.sub.length>2)) CodexMode("toc");
		CodexMode("tocscan");
		fdjtUI.cancel(evt);}}
	Codex.tocJump=tocJump;

	function generate_subsections(headinfo) {
	    var sub=headinfo.sub;
	    if ((!(sub)) || (!(sub.length))) return false;
	    var div=fdjtDOM("div.sub");
	    var i=0; var n=sub.length;
	    while (i<n) {
		var subinfo=sub[i];
		var subspan=fdjtDOM("A.sectname",subinfo.title);
		subspan.frag=subinfo.frag;
		subspan.name="SBR"+subinfo.frag;
		fdjtDOM(div,((i>0)&&" \u00b7 "),subspan);
		i++;}
	    return div;}
	
	function generate_spanbar(headinfo){
	    var spanbar=fdjtDOM("div.spanbar.codexslice");
	    var spans=fdjtDOM("div.spans");
	    var start=headinfo.starts_at;
	    var end=headinfo.ends_at;
	    var len=end-start;
	    var subsections=headinfo.sub; var last_info;
	    var sectnum=0; var percent=0;
	    spanbar.starts=start; spanbar.ends=end;
	    if ((!(subsections)) || (subsections.length===0))
		return false;
	    var progress=fdjtDOM("div.progressbox","\u00A0");
	    var range=false; var lastspan=false;
	    fdjtDOM(spanbar,spans);
	    fdjtDOM(spans,range,progress);
	    progress.style.left="0%";
	    if (range) range.style.left="0%";
	    var i=0; while (i<subsections.length) {
		var spaninfo=subsections[i++];
		var subsection=document.getElementById(spaninfo.frag);
		var spanstart; var spanend; var addname=true;
		if ((sectnum===0) && ((spaninfo.starts_at-start)>0)) {
		    /* Add 'fake section' for the precursor of the first actual section */
		    spanstart=start;  spanend=spaninfo.starts_at;
		    spaninfo=headinfo;
		    subsection=document.getElementById(headinfo.frag);
		    i--; sectnum++; addname=false;}
		else {
		    spanstart=spaninfo.starts_at; spanend=spaninfo.ends_at;
		    sectnum++;}
		var span=generate_span(
		    sectnum,subsection,spaninfo.title,spanstart,spanend,len,
		    ((addname)&&("SBR"+spaninfo.frag)),start);
		lastspan=span;
		spans.appendChild(span);
		if (addname) {
		    var anchor=fdjtDOM("A.codextitle",spaninfo.title);
		    anchor.name="SBR"+spaninfo.frag;
		    spans.appendChild(anchor);}
		last_info=spaninfo;}
	    if ((end-last_info.ends_at)>0) {
		/* Add 'fake section' for the content after the last
		 * actual section */
		var span=generate_span
		(sectnum,head,headinfo.title,last_info.ends_at,end,len,start);
		spanbar.appendChild(span);}    
	    return spanbar;}
	
	function generate_span(sectnum,subsection,title,spanstart,spanend,len,name,pstart){
	    var spanlen=spanend-spanstart;
	    var anchor=fdjtDOM("A.brick","\u00A0");
	    var span=fdjtDOM("DIV.codexhudspan",anchor);
	    var width=(Math.round(100000000*(spanlen/len))/1000000);
	    var left=(Math.round(100000000*((spanstart-pstart)/len))/1000000);
	    span.style.left=left+"%";
	    span.style.width=width+"%";
	    span.title=(title||"section")+
		" ("+Math.round(left)+"%-"+(Math.round(left+width))+"%)";
	    span.frag=subsection.id;
	    if (name) anchor.name=name;
	    return span;}
	CodexTOC.id="$Id$";
	CodexTOC.version=parseInt("$Revision$".slice(10,-1));

	var dropClass=fdjtDOM.dropClass;
	var addClass=fdjtDOM.addClass;
	var getChildren=fdjtDOM.getChildren;
	function updateTOC(prefix,head,container){
	    if (!(prefix)) prefix="CODEXTOC4";
	    var cur=((container)?(getChildren(container,".cur")):
		     (getChildren(document.body,".cur")));
	    var live=((container)?(getChildren(container,".live")):
		      (getChildren(document.body,".live")));
	    var cxt=((container)?(getChildren(container,".cxt")):
		     (getChildren(document.body,".cxt")));
	    dropClass(cur,"cur");
	    dropClass(live,"live");
	    dropClass(cxt,"cxt");
	    if (!(head)) return;
	    var base_elt=document.getElementById(prefix+head.frag);
	    var toshow=[]; var base_info=head;
	    while (head) {
		var tocelt=document.getElementById(prefix+head.frag);
		var spans=document.getElementsByName("SBR"+head.frag);
		addClass(spans,"live");
		toshow.push(tocelt);
		head=head.head;}
	    var n=toshow.length-1;
	    if ((base_info.sub)&&(base_info.sub.length))
		addClass(base_elt,"cxt");
	    else if (toshow[1]) addClass(toshow[1],"cxt");
	    else {}
	    // Go backwards to accomodate some redisplayers
	    while (n>=0) {addClass(toshow[n--],"live");}
	    addClass(base_elt,"cur");}
	CodexTOC.update=updateTOC;

	return CodexTOC;})();


fdjt_versions.decl("codex",codex_toc_version);
fdjt_versions.decl("codex/toc",codex_toc_version);

/* Emacs local variables
   ;;;  Local variables: ***
   ;;;  compile-command: "cd ..; make" ***
   ;;;  End: ***
*/
/* -*- Mode: Javascript; Character-encoding: utf-8; -*- */

var codex_slices_id="$Id$";
var codex_slices_version=parseInt("$Revision$".slice(10,-1));

/* Copyright (C) 2009-2011 beingmeta, inc.
   This file implements the search component of a 
   Javascript/DHTML UI for reading large structured documents (sBooks).

   For more information on sbooks, visit www.sbooks.net
   For more information on knodules, visit www.knodules.net
   For more information about beingmeta, visit www.beingmeta.com

   This library uses the FDJT (www.fdjt.org) toolkit.
   This file assumes that the sbooks.js file has already been loaded.

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

   Use and redistribution (especially embedding in other
   CC licensed content) is permitted under the terms of the
   Creative Commons "Attribution-NonCommercial" license:

   http://creativecommons.org/licenses/by-nc/3.0/ 

   Other uses may be allowed based on prior agreement with
   beingmeta, inc.  Inquiries can be addressed to:

   licensing@beingmeta.com

   Enjoy!

*/

var sbook_details_icon="detailsicon16x16.png";
var sbook_outlink_icon="outlink16x16.png";
var sbook_small_remark_icon="remarkballoon16x13.png";
var sbook_delete_icon_touch="redx24x24.png";
var sbook_delete_icon="redx16x16.png";
var sbook_edit_icon_touch="remarkedit32x25.png";
var sbook_edit_icon="remarkedit32x25.png";
var sbook_reply_icon_touch="codex/replyballoons41x24.png";
var sbook_reply_icon="codex/replyballoons26x15.png";

(function () {

    var div_threshold=7;
    var debug_locbars=false;
    var odq="\u201c"; var cdq="\u201d";

    function renderNote(info,query,idprefix,standalone){
	var key=info._id;
	var target_id=(info.frag)||(info.id);
	var target=((target_id)&&(fdjtID(target_id)));
	var target_info=Codex.docinfo[target_id];
	var head_info=target_info.head;
	var head=((head_info)&&(head_info.elt));
	var refiners=((query) && (query._refiners));
	var score=((query)&&(query[key]));
	var body=
	    fdjtDOM("div.codexcardbody",
		    // (makelocrule(target_info,target_info.head)),
		    ((info.maker)&&(showglossinfo(info)))," ",
		    ((standalone)&&(showtocloc(target_info))),
		    ((score)&&(showscore(score))),
		    ((info.note)&&(fdjtDOM("span.note",info.note)))," ",
		    ((info.shared)&&(info.shared.length)&&
		     (info.shared.length<div_threshold)&&
		     (showaudience(info.shared)))," ",
		    ((info.excerpts)&&(showexcerpts(info.excerpts)))," ",
		    ((info.links)&&(showlinks(info.links,"span.link")))," ",
		    ((info.attachments)&&
		     (showlinks(info.attachments,"span.attachments")))," ",
		    ((info.shared)&&(info.shared.length)&&
		     (info.shared.length>=div_threshold)&&
		     (showaudience(info.shared))),
		    (((info.tags)||(info.autotags))&&(showtags(info))));
	var div=
	    fdjtDOM(((info.maker) ? "div.codexcard.gloss" : "div.codexcard"),
		    ((head)&&(makeTOCHead(head))),
		    ((head_info)&&(makeIDHead(target,head_info,true))),
		    ((standalone)&&(makelocbar(target_info))),
		    body);
	var makerinfo=((info.maker)&&(fdjtKB.ref(info.maker)));
	var tstamp=info.tstamp||info._modified||info._created;
	if (tstamp)
	    body.title="gloss from "+makerinfo.name+" at "+fdjtTime.shortString(tstamp);
	else div.title=Codex.getTitle(target,true);
	div.about="#"+info.frag;
	// div.setAttribute('about',"#"+info.id);
	if (idprefix) div.id=idprefix+info.id;
	if (info._id) {
	    div.name=div.qref=info._id;
	    div.setAttribute("name",info._id);}
	return div;}
    Codex.renderNote=renderNote;
    
    var prime_thresh=7;
    function getprimetags(info){
	if (info.primetags) return info.primetags;
	var tags=info.tags;
	if (typeof tags==='string') tags=[tags];
	if (tags.length<=prime_thresh) return tags;
	var tagscores=Codex.index.tagweights;
	var prime=[].concat(info.tags);
	prime.sort(function(t1,t2){
	    var s1=tagscores[t1]; var s2=tagscores[t2];
	    if ((s1)&&(s2)) {
		if (s1<s2) return -1;
		else if (s1>s2) return 1;
		else return 0;}
	    else if (s1) return -1;
	    else if (s3) return 1;
	    else return 0;});
	info.primetags=prime.slice(0,prime_thresh);
	return info.primetags;}

    var show_tag_thresh=7;

    var expander_toggle=fdjtUI.Expansion.toggle;
    function tagexpand_click(evt){
	return expander_toggle(evt);}

    var combineTags=Knodule.combineTags;
    
    function showtags(info){
	var ctags=info.tags;
	var gtags=info.glosstags;
	var atags=info.autotags;
	var tags; var scores;
	if ((typeof ctags === 'string')||(ctags instanceof String))
	    ctags=[ctags];
	if (!((atags)||(gtags))) {
	    tags=ctags; scores=tags.scores;}
	else if (info.alltags) {
	    // This is where the combination of tags is cached
	    tags=info.alltags; scores=tags.scores;}
	else {
	    // Sort the automatic tags if needed
	    if ((atags)&&(!(atags.sorted))) {
		var weights=Codex.index.tagweights;
		atags.sort(function(t1,t2){
		    var v1=weights[t1], v2=weights[t2];
		    if ((v1)&&(v2)) {
			if (v1<v2) return -1;
			else if (v1>v2) return 1;
			else return 0;}
		    else if (v1) return 1;
		    else return -1;});
		atags.sorted=true;}
	    tags=info.alltags=combineTags([ctags,gtags,atags]);
	    scores=tags.scores;}
	var tagcount=0;
	var countspan=fdjtDOM("span.count");
	var tagicon=fdjtDOM.Image
	(cxicon("TagIcon16x16.png"),"img.tagicon","tags");
	var span=fdjtDOM("span.tags.fdjtexpands",tagicon);
	var tagspan=span;
	var controller=false;
	var i=0; var lim=tags.length;
	while (i<tags.length) {
	    var tag=tags[i]; var score=((scores)&&(scores[tag]))||false;
	    if ((typeof tag === 'string')&&(tag.indexOf('@')>=0))
		tag=fdjtKB.ref(tag)||tag;
	    var togo=tags.length-i;
	    if ((!controller)&&((!(score))||(score<=1))&&
		(i>show_tag_thresh)&&(togo>4)) {
		controller=fdjtDOM("span.controller",
				   "all ",tags.length," tags",
				   fdjtDOM("span.whenexpanded","-"),
				   fdjtDOM("span.whencollapsed","+"));
		var subspan=fdjtDOM("span.whenexpanded");
		controller.onclick=tagexpand_click;
		fdjtDOM(span," ",controller," ",subspan);
		tagspan=subspan;}
	    fdjtDOM.append(tagspan,((i>0)?" \u00b7 ":" "),Knodule.HTML(tag));
	    i++;}
	return span;}
    function showaudience(tags){
	if (!(tags instanceof Array)) tags=[tags];
	var span=fdjtDOM(
	    ((tags.length>=div_threshold)?"div.shared":"span.shared"),
	    ((tags.length>=div_threshold)&&
	     (fdjtDOM("span.count",tags.length, " outlets"))));
	var i=0; var lim=tags.length;
	// This might do some kind of more/less controls and sorted
	// or cloudy display
	while (i<tags.length) {
	    var tag=tags[i]; var info=fdjtKB.ref(tag);
	    fdjtDOM.append(span," ",fdjtDOM("span.outlet","\u2192",info.name));
	    i++;}
	return span;}
    function showlinks(refs,spec){
	var span=fdjtDOM(spec);
	for (url in refs) {
	    if (url[0]==='_') continue;
	    var urlinfo=refs[url];
	    var title; var icon=sbicon("outlink16x8.png");
	    if (typeof urlinfo === 'string') title=urlinfo;
	    else {
		title=urlinfo.title;
		icon=urlinfo.icon;}
	    var image=fdjtDOM.Image(icon);
	    var anchor=(fdjtDOM.Anchor(url,{title:url},title,image));
	    anchor.target='_blank';
	    fdjtDOM(span,anchor,"\n");}
	return span;}
    function showexcerpts(excerpts){
	if (typeof excerpts==='string')
	    return fdjtDOM("span.excerpt",odq,excerpts,cdq);
	else {
	    var espan=fdjtDOM("div.excerpts");
	    var i=0; var lim=excerpts.length;
	    while (i<lim)
		fdjtDOM(espan,
			((i>0)&&" "),
			fdjtDOM("span.excerpt",odq,excerpts[i++],cdq));
	    return espan;}}
    function showscore(score){
	var scorespan=fdjtDOM("span.score");
	var score=query[key]; var k=0;
	while (k<score) {fdjtDOM(scorespan,"*"); k++;}
	return scorespan;}
    function makebutton(fn,alt,title,mouse_icon,touch_icon){
	var span=
	    fdjtDOM("span.isclickable",
		    (fdjtDOM.Image(sbicon(mouse_icon),"img.button.mouseicon",
				   alt,title)),
		    (fdjtDOM.Image(sbicon(touch_icon),"img.button.touchicon",
				   alt,title)));
	span.onclick=fn;
	return span;}
    function showglossinfo(info) {
	var user=info.maker;
	var feed=info.feed||false;
	var userinfo=Codex.sourcekb.map[user];
	var feedinfo=Codex.sourcekb.map[feed];
	var agestring=timestring(info._modified||info._created);
	var mouse_icon=
	    ((user===Codex.user._id)?sbook_edit_icon:sbook_reply_icon);
	var touch_icon=
	    ((user===Codex.user._id)?sbook_edit_icon_touch:
	     sbook_reply_icon_touch);			
	var tool=fdjtDOM(
	    "span.tool",fdjtDOM("span.age",agestring),
	    fdjtDOM.Image(sbicon(mouse_icon),"img.button.mouseicon"),
	    fdjtDOM.Image(sbicon(touch_icon),"img.button.touchicon"));
	tool.title=(((user===Codex.user)||(user===Codex.user._id))?
		    ("edit this gloss"):
		    ("relay/reply to this gloss"));
	var picinfo=getpicinfo(info);
	var overdoc=getoverdoc(info);
	
	return [((picinfo)?
		 (fdjtDOM.Image(picinfo.src,picinfo.classname,picinfo.alt)):
		 (getfakepic(info.maker,"div.sourcepic"))),
		((overdoc)&&(overdoc.name)&&
		 (fdjtDOM("span.overdoc",(overdoc.name)))),
		((overdoc)&&(overdoc.name)&&(" \u00b7 ")),
		(((!(overdoc))&&(userinfo)&&
		  ((userinfo.name)||(userinfo.userid)))&&
		 (fdjtDOM("span.user",((userinfo.name)||(userinfo.userid))))),
		((!(overdoc))&&(userinfo)&&
		 ((userinfo.name)||(userinfo.userid))&&
		 (" \u2014 ")),
		tool];}

    function getoverdoc(info){
	if (info.sources) {
	    var sources=info.sources;
	    if (typeof sources === 'string') sources=[sources];
	    var i=0; var lim=sources.length;
	    while (i<lim) {
		var source=fdjtKB.ref(sources[i++]);
		if ((source)&&(source.kind===':OVERLAY'))
		    return source;}
	    return false;}
	else return false;}

    function getfakepic(maker,spec){
	var userinfo=fdjtKB.ref(maker);
	var pic=fdjtDOM(spec||"div.sbooksourcepic",
			((userinfo.name)?
			 (fdjtString.getInitials(userinfo.name)):
			 "?"));
	return pic;}

    function getpicinfo(info){
	if (info.pic) return {src: info.pic,alt: info.pic};
	if (info.sources) {
	    var sources=info.sources;
	    if (typeof sources==='string') sources=[sources];
	    var i=0; var lim=sources.length;
	    while (i<lim) {
		var source=fdjtKB.ref(sources[i++]);
		if ((source)&&(source.kind===':OVERLAY')&&(source.pic))
		    return { src: source.pic, alt: source.name,
			     classname: "img.glosspic.sourcepic"};}}
	if (info.links) {
	    var links=info.links;
	    var i=0; var lim=links.length;
	    while (i<lim) {
		var link=links[i++];
		if (link.href.search(/\.(jpg|png|gif|jpeg)$/i)>0)
		    return { src: link.href, alt: "graphic",
			     classname: "img.glosspic"};}}
	if (info.shared) {
	    var outlets=info.shared;
	    if (typeof outlets==='string') outlets=[outlets];
	    var i=0; var lim=outlets.length;
	    while (i<lim) {
		var outlet=fdjtKB.ref(outlets[i++]);
		if ((outlet)&&(outlet.kind===':OVERLAY')&&(outlet.pic))
		    return { src: outlet.pic, alt: outlet.name,
			     classname: "img.glosspic.sourcepic"};}}
	if (info.maker) {
	    var userinfo=fdjtKB.ref(info.maker);
	    if (userinfo.pic)
		return { src: userinfo.pic, alt: userinfo.name,
			 classname: "img.glosspic.userpic"};
	    else if (userinfo.fbid)
		return {
		    src: "https://graph.facebook.com/"+
			userinfo.fbid+"/picture?type=square",
		    classname: "img.glosspic.userpic.fbpic"};
	    else return false;}
	else return false;}

    var months=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
    function timestring(tick){
	var now=fdjtTime.tick();
	if ((now-tick)<(12*3600)) {
	    var date=new Date(1000*tick);
	    var hour=date.getHours();
	    var minute=date.getMinutes();
	    return ""+hour+":"+((minute<10)?"0":"")+minute;}
	else {
	    var date=new Date(1000*tick);
	    var year=date.getFullYear();
	    var month=date.getMonth();
	    var date=date.getDate();
	    var shortyear=year%100;
	    if (year<10)
		return ""+date+"/"+months[month]+"/0"+year;
	    else return ""+date+"/"+months[month]+"/"+year;}}

    function makelocbar(target_info,cxt_info){
	var locrule=fdjtDOM("HR");
	var locbar=fdjtDOM("DIV.locbar",locrule);
	var target_start=target_info.starts_at;
	var target_end=target_info.ends_at;
	var target_len=target_end-target_start;
	if (!(cxt_info)) cxt_info=Codex.docinfo[document.body.id];
	var cxt_start=cxt_info.starts_at;
	var cxt_end=cxt_info.ends_at;
	var cxt_len=cxt_end-cxt_start;
	if (debug_locbars)
	    locbar.setAttribute(
		"debug","ts="+target_start+"; te="+target_end+"; cl="+cxt_len);
	locrule.style.width=((target_len/cxt_len)*100)+"%";
	locrule.style.left=(((target_start-cxt_start)/cxt_len)*100)+"%";
	var id=target_info.id||target_info.frag;
	if (id) {
	    locbar.about="#"+id;
	    locbar.title=sumText(fdjtID(id));}
	return locbar;}
    function showtocloc(target_info){
	var head=((target_info.toclevel)?(target_info):(target_info.head));
	var heads=head.heads;
	var anchor=fdjtDOM.Anchor(
	    "javascript:Codex.JumpTo('"+(head.frag||head.id)+"');","a.headref",
	    fdjtDOM("span.spacer","\u00A7"),
	    head.title);
	var title="jump to "+head.title;
	var i=heads.length-1; 
	while (i>0) {
	    var head=heads[i--]; title=title+"// "+head.title;}
	anchor.title=title;
	return [" ",anchor];}

    function makelocspan(target_info,cxtinfo){
	if (!(cxtinfo)) cxtinfo=Codex.docinfo[(Codex.body||document.body).id];
	var locrule=fdjtDOM("div.locrule");
	var cxt_start=cxtinfo.starts_at;
	var cxt_end=cxtinfo.ends_at;
	var cxt_len=cxt_end-cxt_start;
	var location_start=target_info.starts_at-cxt_start;
	var location_len=target_info.ends_at-target_info.starts_at;
	locrule.setAttribute("about","#"+(target_info.id||target_info.frag));
	locrule.title='click or hold to glimpse';
	locrule.style.width=((location_len/cxt_len)*100)+"%";
	locrule.style.left=((location_start/cxt_len)*100)+"%";
	return locrule;}
    function makelocrule(target_info,cxtinfo,spec){
	if (!(cxtinfo)) cxtinfo=Codex.docinfo[(Codex.body||document.body).id];
	var locrule=fdjtDOM(spec||"hr.locrule");
	var cxt_start=cxtinfo.starts_at;
	var cxt_end=cxtinfo.ends_at;
	var cxt_len=cxt_end-cxt_start;
	var target_start=target_info.starts_at-cxt_start;
	var target_len=target_info.ends_at-target_info.starts_at;
	var locstring="~"+Math.ceil(target_len/5)+ " words long ~"+
	    Math.ceil((target_start/cxt_len)*100)+"% along";
	locrule.setAttribute("about","#"+(target_info.id||target_info.frag));
	locrule.locstring=locstring+".";
	locrule.title=locstring+": click or hold to glimpse";
	locrule.style.width=((target_len/cxt_len)*100)+"%";
	locrule.style.left=((target_start/cxt_len)*100)+"%";
	return locrule;}

    function editicon_ontap(evt){
	var target=fdjtUI.T(evt);
	var card=fdjtDOM.getParent(target,'.codexcard');
	var gloss=((card)&&(card.name)&&(fdjtKB.ref(card.name,Codex.glosses)));
	Codex.setGlossTarget(gloss);
	CodexMode("addgloss");}
    function replyicon_ontap(evt){
	var target=fdjtUI.T(evt);
	var card=fdjtDOM.getParent(target,'.codexcard');
	var gloss=((card)&&(card.name)&&(fdjtKB.ref(card.name,Codex.glosses)));
	Codex.setGlossTarget(gloss,Codex.getGlossForm(gloss,true));
	CodexMode("addgloss");}

    function relayoredit_gloss(evt){
	var scan=fdjtUI.T(evt);
	fdjtUI.cancel(evt);
	while (scan) {
	    if (scan.qref) break;
	    else scan=scan.parentNode;}
	if (!(scan)) return;
	var qref=scan.qref;
	var gloss=Codex.glosses.ref(qref);
	var frag=gloss.get("frag");
	Codex.setGlossTarget(gloss);
	CodexMode("addgloss");}

    function sourceIcon(info){
	if (info) return info.pic;}
    
    function sbicon(name,suffix) {return Codex.graphics+name+(suffix||"");}
    function cxicon(name,suffix) {
	return Codex.graphics+"codex/"+name+(suffix||"");}

    // Displayings sets of notes organized into threads

    function sortbyloctime(x,y){
	if (x.frag===y.frag) {
	    if ((x.tstamp)&&(y.tstamp)) {
		if (x.tstamp<y.tstamp) return -1;
		else if (x.tstamp>y.tstamp) return 1;
		else return 0;}
	    else if (x.tstamp) return 1;
	    else if (y.tstamp) return -1;
	    else return 0;}
	else if (x.ends_at<=y.starts_at) return 1;
	else if (x.starts_at>=y.ends_at) return -1;
	else if ((x.ends_at-x.starts_at)>(y.ends_at-y.starts_at))
	    return 1;
	else return -1;}

    function showSlice(results,div,scores,sort){
	var notes=new Array(results.length);
	var i=0; var lim=results.length;
	while (i<lim) {
	    var r=results[i];
	    if (typeof r === 'string') {
		var ref=Codex.docinfo[r]||Codex.glosses.ref(r);
		if (!(ref)) fdjtLog("No resolution for %o",r);
		notes[i]=ref;}
	    else notes[i]=r;
	    i++;}
	if (!(sort)) {}
	else if (scores)
	    notes.sort(function(n1,n2){
		var s1=(scores[n1._id]);
		var s2=(scores[n2._id]);
		if ((s1)&&(s2)) {
		    if (s1>s2) return -1;
		    else if (s2>s1) return 1;}
		else if (s1) return -1;
		else if (s2) return 1;
		if (n1.starts_at<n2.starts_at) return -1;
		else if (n1.starts_at>n2.starts_at) return 1;
		else if (n1.ends_at<n2.ends_at) return -1;
		else if (n1.ends_at>n2.ends_at) return 1;
		else if ((n1.tstamp)&&(n2.tstamp)) {
		    if (n1.tstamp>n2.tstamp) return -1;
		    else if (n1.tstamp>n2.tstamp) return 1;
		    else return 0;}
		else if (n1.tstamp) return 1;
		else if (n2.tstamp) return -1;
		else return 0;});
	else notes.sort(function(n1,n2){
	    if (n1.starts_at<n2.starts_at) return 1;
	    else if (n1.starts_at>n2.starts_at) return -1;
	    else if (n1.ends_at<n2.ends_at) return 1;
	    else if (n1.ends_at>n2.ends_at) return -1;
	    else if ((n1.tstamp)&&(n2.tstamp)) {
		if (n1.tstamp>n2.tstamp) return -1;
		else if (n1.tstamp>n2.tstamp) return 1;
		else return 0;}
	    else if (n1.tstamp) return 1;
	    else if (n2.tstamp) return -1;
	    else return 0;});
	
	var headelt=false; var threadelt=false;
	var curhead=false; var curinfo=false;
	var i=0; var len=notes.length; while (i<len) {
	    var note=notes[i++];
	    var frag=note.id||note.frag;
	    if (!(frag)) continue;
	    var target=fdjtID(frag);
	    var docinfo=Codex.docinfo[target.id];
	    var headinfo=docinfo.head;
	    var head=document.getElementById(headinfo.frag);
	    // var tochead=makeTOCHead(head);
	    if (curinfo!==docinfo) {
		if (headinfo!==curhead) {
		    headelt=fdjtDOM("div.codexthread.tocthread"); // ,tochead
		    headelt.frag=headinfo.frag;
		    fdjtDOM.append(div,headelt);
		    curhead=headinfo;}
		threadelt=fdjtDOM("div.codexthread.idthread");
		// ,makeIDHead(target,headinfo,true)
		threadelt.about="#"+frag;
		threadelt.title=Codex.getTitle(target,true);
		fdjtDOM.append(headelt,threadelt);
		curinfo=docinfo;}
	    fdjtDOM.append(threadelt,renderNote(note));}
	return div;}
    Codex.UI.showSlice=showSlice;

    function sumText(target){
	var title=Codex.getTitle(target,true);
	if (title.length<40) return title;
	/* title.slice(0,40)+"\u22ef "; */
	else return title;}
    
    function makeTOCHead(target,head){
	if (!(head)) head=Codex.getHead(target);
	var basespan=fdjtDOM("span");
	basespan.title='this location in the structure of the book';
	var title=Codex.getTitle(target,true);
	var info=Codex.docinfo[target.id];
	var head_info=Codex.docinfo[head.id];
	if (target!==head) {
	    var paratext=
		fdjtDOM.Anchor("javascript:Codex.JumpTo('"+target.id+"');",
			       "a.paratext",
			       fdjtDOM("span.spacer","\u00B6"),
			       sumText(target));
	    paratext.title='(click to jump to this passage) '+title;
	    fdjtDOM(basespan,paratext," ");}
	if (head) {
	    var text=sumText(head);
	    var headtext=
		fdjtDOM.Anchor("javascript:Codex.JumpTo('"+head.id+"');",
			       "a.headtext",
			       fdjtDOM("span.spacer","\u00A7"),
			       text);
	    var curspan=fdjtDOM("span.head",headtext);
	    headtext.title='jump to the section: '+text;
	    fdjtDOM.append(basespan," ",curspan);
	    var heads=Codex.Info(head).heads;
	    if (heads) {
		var j=heads.length-1; while (j>=0) {
		    var hinfo=heads[j--]; var elt=fdjtID(hinfo.frag);
		    if ((!(elt))||(!(hinfo.title))||
			(elt===Codex.root)||(elt===document.body))
			continue;
		    var anchor=
			fdjtDOM.Anchor(
			    "javascript:Codex.JumpTo('"+hinfo.frag+"');",
			    "a.headtext",
			    fdjtDOM("span.spacer","\u00A7"),
			    hinfo.title);
		    var newspan=fdjtDOM("span.head"," ",anchor);
		    anchor.title=
			((hinfo.title)?('jump to the section: '+hinfo.title):
			 "(jump to this section)");
		    if (target===head) fdjtDOM(curspan,newspan);
		    else fdjtDOM(curspan," \u22ef ",newspan);
		    curspan=newspan;}}}
	var tochead=fdjtDOM("div.tochead",
			    makelocrule(info,false),
			    basespan);
	return tochead;}

    function makeIDHead(target,headinfo,locrule){
	var info=Codex.docinfo[target.id];
	var headinfo=info.head;
	var tochead=fdjtDOM("div.idhead",
			    makelocrule(info,headinfo),
			    fdjtDOM("span.spacer","\u00b6"),
			    fdjtDOM("span",sumText(target)));
	var title=Codex.getTitle(target,true);
	return tochead;}

    function findTOCref(div,ref,loc) {
	var children=div.childNodes;
	if (!(children)) return false;
	var i=0; var lim=children.length;
	while (i<lim) {
	    var child=children[i++];
	    if (!(child.nodeType===1)) continue;
	    else if (child.tocref===ref) return child;
	    else if (child.starts>loc) return child;
	    else continue;}
	return false;}

    function addToSlice(note,div,query){
	var frag=(note.id||note.frag);
	var eltinfo=Codex.docinfo[frag];
	var about=document.getElementById(frag);
	var headinfo=((eltinfo.toclevel)?(eltinfo):(eltinfo.head));
	var headid=headinfo.frag;
	var head=document.getElementById(headid);
	var starts=eltinfo.starts_at;
	var head_starts=headinfo.starts_at;
	var insertion=false; var insdiff=0;
	var headthread=findTOCref(div,headid,head_starts);
	if ((!(headthread))||(headthread.tocref!==headid)) {
	    var insertbefore=headthread;
	    headthread=fdjtDOM("div.codexthread.tocthread");
	    // ,makeTOCHead(head,head)
	    headthread.tocref=headid; headthread.starts=head_starts;
	    if (insertbefore) fdjtDOM.insertBefore(insertbefore,headthread);
	    else fdjtDOM.append(div,headthread);}
	var idthread=((frag===headid)?(headthread):
		      (findTOCref(headthread,frag,starts)));
	if ((!(idthread))||(idthread.tocref!==frag)) {
	    var insertbefore=idthread;
	    idthread=fdjtDOM("div.codexthread.idthread");
	    idthread.tocref=frag; idthread.starts=starts; idthread.about="#"+frag;
	    idthread.title=Codex.getTitle(about,true);
	    idthread.setAttribute("locref",frag);
	    idthread.setAttribute("locinfo",starts);
	    if (insertbefore) fdjtDOM.insertBefore(insertbefore,idthread);
	    else fdjtDOM.append(headthread,idthread);}
	var tstamp=note.tstamp; var qid=note._id;
	var children=headthread.childNodes;
	var ishead=(frag===headid);
	var i=0; var lim=children.length;
	while (i<lim) {
	    var child=children[i++];
	    if (child.nodeType!==1) continue;
	    if ((ishead)&&(fdjtDOM.hasClass(child,"codexthread"))) {
		fdjtDOM.insertBefore(child,renderNote(note));
		return;}
	    // If unrelated, continue
	    if (!((fdjtDOM.hasClass(child,"codexcard"))||
		  (fdjtDOM.hasClass(child,"codexthread"))))
		continue;
	    // If the same thing, replace
	    if (child.qref===qid) {
		fdjtDOM.replace(child,renderNote(note));
		return;}
	    // if you're earlier, insert yourself and return
	    if (tstamp<=child.tstamp) {
		fdjtDOM.insertBefore(child,renderNote(note));
		return;}
	    else continue;}
	fdjtDOM.append(idthread,renderNote(note));}
    Codex.UI.addToSlice=addToSlice;

    Codex.nextSlice=function(start){
	var slice=fdjtDOM.getParent(start,".codexslice");
	var scan=fdjtDOM.forwardElt(start); var ref=false;
	while (scan) {
	    if (((scan.about)||
		 ((scan.getAttribute)&&(scan.getAttribute("about"))))&&
		((fdjtDOM.hasClass(scan,"codexcard"))||
		 (fdjtDOM.hasClass(scan,"passage"))))
		break;
	    else scan=fdjtDOM.forwardElt(scan);}
	if (fdjtDOM.hasParent(scan,slice)) return scan;
	else return false;};
    Codex.prevSlice=function(start){
	var slice=fdjtDOM.getParent(start,".codexslice");
	var scan=fdjtDOM.backwardElt(start); var ref=false;
	while (scan) {
	    if (((scan.about)||
		 ((scan.getAttribute)&&(scan.getAttribute("about"))))&&
		((fdjtDOM.hasClass(scan,"codexcard"))||
		 (fdjtDOM.hasClass(scan,"passage"))))
		break;
	    else scan=fdjtDOM.backwardElt(scan);}
	if (fdjtDOM.hasParent(scan,slice)) return scan;
	else return false;};

    /* Selecting a subset of glosses to display */

    var hasClass=fdjtDOM.hasClass;

    function selectSourcesRecur(thread,sources){
	var empty=true; var children=thread.childNodes;
	var i=0; var lim=children.length;
	while (i<children.length) {
	    var child=children[i++];
	    if (child.nodeType!==1) continue;
	    if (hasClass(child,"codexcard")) {
		var gloss=(child.qref)&&Codex.glosses.map[child.qref];
		if (!(gloss)) fdjtDOM.dropClass(child,"sourced");
		else if ((fdjtKB.contains(sources,gloss.maker))||
			 (fdjtKB.overlaps(sources,gloss.sources))||
			 (fdjtKB.overlaps(sources,gloss.shared))) {
		    fdjtDOM.addClass(child,"sourced");
		    empty=false;}
		else fdjtDOM.dropClass(child,"sourced");}
	    else if (hasClass(child,"codexthread")) {
		if (!(selectSourcesRecur(child,sources)))
		    empty=false;}
	    else {}}
	if (!(empty)) fdjtDOM.addClass(thread,"sourced");
	else fdjtDOM.dropClass(thread,"sourced");
	return empty;}

    function selectSources(results_div,sources){
	if (!(sources)) {
	    fdjtDOM.dropClass(results_div,"sourced");
	    fdjtDOM.dropClass(fdjtDOM.$(".sourced",results_div),"sourced");
	    return;}
	selectSourcesRecur(results_div,sources);
	if (Codex.target) scrollGlosses(Codex.target,results_div);}
    Codex.UI.selectSources=selectSources;

    /* Scrolling slices */

    function scrollGlosses(elt,glosses,top){
	if (!(elt.id)) elt=getFirstID(elt);
	var info=Codex.docinfo[elt.id];
	var targetloc=((info)&&(info.starts_at))||(elt.starts_at);
	if (targetloc) {
	    var scrollto=getFirstElt(glosses,targetloc);
	    if ((scrollto)&&((top)||(!(fdjtDOM.isVisible(scrollto))))) {
		if ((Codex.scrollers)&&(glosses.id)&&
		    (Codex.scrollers[glosses.id])) {
		    var scroller=Codex.scrollers[glosses.id];
		    scroller.scrollToElement(scrollto);}
		else scrollto.scrollIntoView(true);}}}
    Codex.UI.scrollGlosses=scrollGlosses;
    
    function getFirstID(node){
	if (node.id) return node;
	else if (node.childNodes) {
	    var children=node.childNodes;
	    var i=0; var lim=children.length;
	    while (i<lim) {
		var child=children[i++];
		if (child.nodeType===1) {
		    var found=getFirstID(child);
		    if (found) return found;}}
	    return false;}
	else return false;}

    function getFirstElt(glosses,location){
	var children=glosses.childNodes; var last=false;
	var i=0; var lim=children.length;
	while (i<lim) {
	    var child=children[i++];
	    if (child.nodeType!==1) continue;
	    else if (!(child.starts)) continue;
	    else if (child.starts===location)
		return child;
	    else if (child.starts>location) {
		if (last)
		    return getFirstElt(last,location)||last;
		else return last;}
	    else last=child;}
	if (last) getFirstElt(last,location);
	return false;}
    
    function getScrollOffset(elt,inside){
	if (elt.parentNode===inside) {
	    var children=inside.childNodes;
	    var i=0; var lim=children.length; var off=0;
	    while (i<lim) {
		var child=children[i++];
		if (child===elt) return off;
		if (child.offsetHeight) off=off+child.offsetHeight;}
	    return off;}
	else return getScrollOffset(elt,elt.parentNode)+
	    getScrollOffset(elt.parentNode,inside);}

    /* Results handlers */

    function setupSummaryDiv(div){
	Codex.UI.addHandlers(div,'summary');}
    Codex.UI.setupSummaryDiv=setupSummaryDiv;
    
})();

fdjt_versions.decl("codex",codex_slices_version);
fdjt_versions.decl("codex/slices",codex_slices_version);

/* Emacs local variables
   ;;;  Local variables: ***
   ;;;  compile-command: "cd ..; make" ***
   ;;;  End: ***
*/
/* -*- Mode: Javascript; Character-encoding: utf-8; -*- */

var codex_social_id="$Id$";
var codex_social_version=parseInt("$Revision$".slice(10,-1));

/* Copyright (C) 2009-2011 beingmeta, inc.
   This file implements a Javascript/DHTML UI for reading
    large structured documents (sBooks).

   For more information on sbooks, visit www.sbooks.net
   For more information on knodules, visit www.knodules.net
   For more information about beingmeta, visit www.beingmeta.com

   This library uses the FDJT (www.fdjt.org) toolkit.

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

    Use and redistribution (especially embedding in other
      CC licensed content) is permitted under the terms of the
      Creative Commons "Attribution-NonCommercial" license:

          http://creativecommons.org/licenses/by-nc/3.0/ 

    Other uses may be allowed based on prior agreement with
      beingmeta, inc.  Inquiries can be addressed to:

       licensing@beingmeta.com

   Enjoy!

*/

(function(){

    var sbook_sources=false;
    var sbook_glosses_target=false;
    var sbookGlossesHUD=false;
    var sbookSourceHUD=false;

    // The highlighted glossmark
    var sbook_glossmark=false;
    var sbook_glossmark_qricons=false;

    // The glosses element
    var CodexHUDglosses=false;
    // The user/tribe bar
    var CodexHUDsocial=false;

    /* Social UI components */

    function sbicon(name,suffix) {return Codex.graphics+name+(suffix||"");}
    function cxicon(name,suffix) {
	return Codex.graphics+"codex/"+name+(suffix||"");}
    

    function addSource(info,withgloss){
	if (typeof info === 'string') info=fdjtKB.ref(info);
	var humid=info.humid;
	if (!(info.name)) return;
	if (withgloss) {
	    var icon=fdjtID("SBOOKSOURCEICON"+humid);
	    if (!(icon)) { // Add icon to the sources bar
		var pic=(info.pic)||
		    ((info.fbid)&&
		     ("https://graph.facebook.com/"+info.fbid+
		      "/picture?type=square"));
		var kind=info.kind;
		if (pic) {}
		else if (kind===':CIRCLE')
		    pic=cxicon("sbookscircle50x50.png");
		else if (kind===':OVERDOC')
		    pic=cxicon("sbooksoverdoc50x50.png");
		else {}
		if (pic)
		  icon=fdjtDOM.Image
		    (pic,".button.source",info.name|info.kind,
		     ("click to show/hide glosses from "+info.name));
		else {
		  icon=fdjtDOM("div.button.source",
			       fdjtString.getInitials(info.name));}
		icon.title=info.name;
		icon.oid=info.oid; icon.id="SBOOKSOURCEICON"+humid;
		fdjtDOM(fdjtID("CODEXSOURCES")," ",icon);}}
	var sharetag=fdjtID("SBOOKSHARETAG"+humid);
	if (!(sharetag)) { // Add entry to the share cloud
	    var completion=fdjtDOM("span.completion.cue.source",info.name);
	    completion.id="SBOOKSHARETAG"+humid;
	    completion.setAttribute("value",info._id);
	    completion.setAttribute("key",info.name);}
	var sourcetag=fdjtID("SBOOKSOURCETAG"+humid);
	if (!(sourcetag)) { // Add entry to the share cloud
	    var completion=fdjtDOM("span.completion.source",info.name);
	    completion.id="SBOOKSOURCETAG"+humid;
	    completion.setAttribute("value",info._id);
	    completion.setAttribute("key",info.name);
	    fdjtDOM(fdjtID("CODEXGLOSSCLOUDSOURCES"),completion," ");
	    if (Codex.gloss_cloud)
	      Codex.gloss_cloud.addCompletion(completion);}
	// This is tricky because fdjtID may not work when the full
	//  cloud is not in the DOM for some reason
	var searchtag=
	  fdjtID("CODEXSEARCHSOURCE"+humid)||
	    ((Codex.full_cloud)&&(Codex.full_cloud.getByValue(info._id)));
	if ((!(searchtag))||(searchtag.length===0)) {
	  // Add entry to the search cloud
	  var completion=fdjtDOM("span.completion.source",info.name);
	  completion.id="CODEXSEARCHSOURCE"+humid;
	    completion.setAttribute("value",info._id);
	  completion.setAttribute("key",info.name);
	  fdjtDOM(fdjtID("CODEXSEARCHCLOUD"),completion," ");
	  if (Codex.full_cloud)
	    Codex.full_cloud.addCompletion(completion);}
	return info;};
    Codex.UI.addSource=addSource;
    Codex.UI.addGlossSource=function(info){addSource(info,true);};

    function everyone_ontap(evt)
    {
	evt=evt||event||null;
	var target=fdjtDOM.T(evt);
	// var sources=fdjtDOM.getParent(target,".sbooksources");
	// var glosses=fdjtDOM.getParent(target,".sbookglosses");
	var sources=fdjtID("CODEXSOURCES");
	var glosses=fdjtID("CODEXALLGLOSSES");
	var new_sources=[];
	if ((!(sources))||(!(glosses)))
	    return; /* Warning? */
	if (fdjtDOM.hasClass(target,"selected")) {
	    CodexMode(false);
	    fdjtDOM.cancel(evt);
	    return;}
	var selected=fdjtDOM.$(".selected",sources);
	fdjtLog("Everyone click sources=%o glosses=%o selected=%o/%d",
		sources,glosses,selected,selected.length);
	fdjtDOM.toggleClass(selected,"selected");
	fdjtDOM.addClass(target,"selected");
	Codex.UI.selectSources(glosses,false);
	fdjtDOM.cancel(evt);
    }
    Codex.UI.handlers.everyone_ontap=everyone_ontap;

    function sources_ontap(evt)
    {
	evt=evt||event||null;
	// if (!(Codex.user)) return;
	var target=fdjtDOM.T(evt);
	// var sources=fdjtDOM.getParent(target,".sbooksources");
	// var glosses=fdjtDOM.getParent(target,".sbookglosses");
	var sources=fdjtID("CODEXSOURCES");
	var glosses=fdjtID("CODEXALLGLOSSES");
	var new_sources=[];
	if ((!(sources))||(!(glosses))||(!(target.oid)))
	    return; /* Warning? */
	if ((evt.shiftKey)||(fdjtDOM.hasClass(target,"selected"))) {
	    fdjtDOM.toggleClass(target,"selected");
	    var selected=fdjtDOM.$(".selected",sources);
	    var i=0; var len=selected.length;
	    while (i<len) {
		var oid=selected[i++].oid;
		if (oid) new_sources.push(oid);}}
	else {
	    var selected=fdjtDOM.$(".selected",sources);
	    var i=0; var len=selected.length;
	    while (i<len) fdjtDOM.dropClass(selected[i++],"selected");
	    fdjtDOM.addClass(target,"selected");
	    new_sources=[target.oid];}
	var everyone=fdjtDOM.$(".everyone",sources)[0];
	if (new_sources.length) {
	    if (everyone) fdjtDOM.dropClass(everyone,"selected");
	    Codex.UI.selectSources(glosses,new_sources);}
	else {
	    if (everyone) fdjtDOM.addClass(everyone,"selected");
	    Codex.UI.selectSources(glosses,false);}
	fdjtDOM.cancel(evt);
    }
    Codex.UI.handlers.sources_ontap=sources_ontap;

    Codex.UI.addGlossmark=function(passage){
	var glossmark=fdjtDOM.getChild(passage,".codexglossmark");
	if ((glossmark)&&(glossmark.parentNode===passage))
	    return glossmark;
	var imgsrc=(cxicon("sbookspeople32x32.png"));
	var glossmark=fdjtDOM(
	    "span.codexglossmark",
	    fdjtDOM.Image(imgsrc,"big","glosses"),
	    fdjtDOM.Image(sbicon("Asterisk16x16.png"),"tiny","*"));
	Codex.UI.addHandlers(glossmark,"glossmark");
	fdjtDOM.addClass(passage,"glossed");
	fdjtDOM.prepend(passage,glossmark);
	return glossmark;};
    
    function showGlosses(target) {
	var glosses=Codex.glosses.find('frag',target.codexdupid||target.id);
	var sumdiv=fdjtDOM("div.codexglosses.hudpanel");
	if ((!(glosses))||(!(glosses.length)))
	    fdjtDOM.addClass(sumdiv,"noglosses");
	// Codex.UI.setupSummaryDiv(sumdiv);
	if (glosses) {
	    var i=0; var n=glosses.length;
	    while (i<n) {
		var gloss=fdjtKB.ref(glosses[i++],Codex.glosses);
		if ((!(gloss))||(!(gloss.frag))) continue;
		var card=Codex.renderNote(gloss);
		fdjtDOM(sumdiv,card);}}
	fdjtDOM.replace("CODEXGLOSSES",sumdiv);
	Codex.setTarget(target);
	CodexMode("glosses");}
    Codex.showGlosses=showGlosses;

})();

fdjt_versions.decl("codex",codex_social_version);
fdjt_versions.decl("codex/social",codex_social_version);

/* Emacs local variables
;;;  Local variables: ***
;;;  compile-command: "cd ..; make" ***
;;;  End: ***
*/
/* -*- Mode: Javascript; Character-encoding: utf-8; -*- */

var codex_search_id="$Id$";
var codex_search_version=parseInt("$Revision$".slice(10,-1));

/* Copyright (C) 2009-2011 beingmeta, inc.
   This file implements the search component of a 
   Javascript/DHTML UI for reading large structured documents (sBooks).

   For more information on sbooks, visit www.sbooks.net
   For more information on knodules, visit www.knodules.net
   For more information about beingmeta, visit www.beingmeta.com

   This library uses the FDJT (www.fdjt.org) toolkit.
   This file assumes that the sbooks.js file has already been loaded.

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

   Use and redistribution (especially embedding in other
   CC licensed content) is permitted under the terms of the
   Creative Commons "Attribution-NonCommercial" license:

   http://creativecommons.org/licenses/by-nc/3.0/ 

   Other uses may be allowed based on prior agreement with
   beingmeta, inc.  Inquiries can be addressed to:

   licensing@beingmeta.com

   Enjoy!

*/

(function(){
    Codex.full_cloud=false;
    if (!(Codex.empty_cloud)) Codex.empty_cloud=false;
    if (!(Codex.show_refiners)) Codex.show_refiners=25;
    if (!(Codex.search_gotlucky)) Codex.search_gotlucky=7;
    
    function sbicon(name,suffix) {return Codex.graphics+name+(suffix||"");}
    function cxicon(name,suffix) {
	return Codex.graphics+"codex/"+name+(suffix||"");}

    var addClass=fdjtDOM.addClass;

    /* Query functions */

    /* Set on main search input */
    // id="CODEXSEARCHINPUT" 
    // completions="CODEXSEARCHCLOUD"

    var Query=KnoduleIndex.Query;

    Codex.getQuery=function(){return Codex.query;}
    
    function setQuery(query){
	if (Codex.Trace.search) fdjtLog("Setting working query to %o",query);
	var query=Codex.query=useQuery(query,fdjtID("CODEXSEARCH"));
	if (Codex.mode==="search") {
	    if (query._results.length===0) {}
	    else if ((query._results.length===1)&&
		(document.getElementById(query._results[0]))) {
		Codex.GoTo(query._results[0]);}
	    else if (query._results.length<7)
		showSearchResults();
	    else {}}}

    Codex.setQuery=setQuery;

    function useQuery(query,box_arg){
	var result;
	if (query instanceof Query) result=query;
	else result=Codex.index.Query(query);
	var qstring=result.getString();
	if ((box_arg)&&(typeof box_arg === 'string'))
	    box_arg=document.getElementById(box_arg);
	var box=box_arg||result._box||fdjtID("CODEXSEARCH");
	if ((query.dom)&&(box)&&(box!==query.dom))
	    fdjtDOM.replace(box_arg,query.dom);
	if (qstring===box.getAttribute("qstring")) {
	    fdjtLog("No change in query for %o to %o: %o/%o (%o)",
		    box,result._query,result,result._refiners,qstring);
	    return;}
	if (Codex.Trace.search>1)
	    fdjtLog("Setting query for %o to %o: %o/%o (%o)",
		    box,result._query,result,result._refiners,qstring);
	else if (Codex.Trace.search)
	    fdjtLog("Setting query for %o to %o: %d results/%d refiners (%o)",
		    box,result._query,result._results.length,
		    result._refiners._results.length,qstring);
	var input=fdjtDOM.getChild(box,".searchinput");
	var cloudid=input.getAttribute("completions");
	var resultsid=input.getAttribute("results");
	var qtags=fdjtDOM.getChild(box,".qtags");
	var cloud=((cloudid)&&(fdjtID(cloudid)))||
	    fdjtDOM.getChild(box,".searchcloud");
	var results=((resultsid)&&(fdjtID(resultsid)))||
	    fdjtDOM.getChild(box,".searchresults");
	var resultcount=fdjtDOM.getChild(box,".resultcount");
	var refinecount=fdjtDOM.getChild(box,".refinecount");
	// Update (clear) the input field
	input.value='';
	var elts=result._query; var i=0; var lim=elts.length;
	// Update 'notags' class
	if (elts.length) fdjtDOM.dropClass(box,"notags");
	else addClass(box,"notags");
	// Update the query tags
	var newtags=fdjtDOM("span.qtags");
	while (i<lim) {
	    var tag=elts[i];
	    if (typeof tag === 'string') tag=fdjtKB.ref(tag)||tag;
	    if (i>0) fdjtDOM(newtags," \u00B7 ");
	    if (typeof tag === "string")
		fdjtDOM(newtags,fdjtDOM("span.dterm",tag));
	    else if (tag.name)
		fdjtDOM(newtags,tag.name);
	    else fdjtDOM(newtags,tag);
	    i++;}
	if (qtags.id) newtags.id=qtags.id;
	fdjtDOM.replace(qtags,newtags);
	// Update the results display
	if (result._results.length) {
	    resultcount.innerHTML=result._results.length+
		" passage"+((result._results.length===1)?"":"s");
	    fdjtDOM.dropClass(box,"noresults");}
	else {
	    resultcount.innerHTML="no results";
	    addClass(box,"noresults");}
	// Update the search cloud
	var n_refiners=
	    ((result._refiners)&&(result._refiners._results.length))||0;
	var completions=Codex.queryCloud(result);
	refinecount.innerHTML=n_refiners+
	    ((n_refiners===1)?(" associated tag"):(" associated tags"));
	fdjtDOM.dropClass(box,"norefiners");
	if (cloudid) completions.id=cloudid;
	if (Codex.Trace.search>1)
	    fdjtLog("Setting search cloud for %o to %o",
		    box,completions.dom);
	cloudid=cloud.id;
	fdjtDOM.replace(cloud,completions.dom);
	    completions.complete("");
	if (n_refiners===0) {
	    addClass(box,"norefiners");
	    refinecount.innerHTML="no refiners";}
	result._box=box; box.setAttribute(qstring,qstring);
	Codex.UI.updateScroller(completions.dom);
	return result;}
    Codex.useQuery=useQuery;

    function extendQuery(query,elt){
	var elts=[].concat(query._query);
	if (typeof elt === 'string') 
	    elts.push(fdjtKB.ref(elt)||elt);
	else elts.push(elt);
	return useQuery(query.index.Query(elts),query._box);}
    Codex.extendQuery=extendQuery;

    Codex.updateQuery=function(input_elt){
	var q=Knodule.Query.string2query(input_elt.value);
	if ((q)!==(Codex.query._query))
	    Codex.setQuery(q,false);};

    function showSearchResults(){
	fdjtDOM.replace("CODEXSEARCHRESULTS",Codex.query.showResults());
	CodexMode("searchresults");
	fdjtID("CODEXSEARCHINPUT").blur();
	fdjtID("CODEXSEARCHRESULTS").focus();
	Codex.UI.updateScroller(fdjtID("CODEXSEARCHRESULTS"));}
    Codex.showSearchResults=showSearchResults;

    /* Call this to search */

    function startSearch(tag){
	setQuery([tag]);
	CodexMode("search");}
    Codex.startSearch=startSearch;

    /* Text input handlers */

    var _sbook_searchupdate=false;
    var _sbook_searchupdate_delay=200;
    
    function searchInput_keyup(evt){
	evt=evt||event||null;
	var ch=evt.charCode||evt.keyCode;
	var target=fdjtDOM.T(evt);
	// fdjtLog("Input %o on %o",evt,target);
	// Clear any pending completion calls
	if ((ch===13)||(ch===13)||(ch===59)||(ch===93)) {
	    var qstring=target.value;
	    if (fdjtString.isEmpty(qstring)) showSearchResults();
	    else {
		var completeinfo=queryCloud(Codex.query);
		if (completeinfo.timer) {
		    clearTimeout(completeinfo.timer);
		    completeinfo.timer=false;}
		var completions=completeinfo.complete(qstring);
		if (completions.length) {
		    var value=completeinfo.getValue(completions[0]);
		    setQuery(extendQuery(Codex.query,value));}}
	    fdjtDOM.cancel(evt);
	    if ((Codex.search_gotlucky) && 
		(Codex.query._results.length>0) &&
		(Codex.query._results.length<=Codex.search_gotlucky))
		showSearchResults();
	    else {
		/* Handle new info */
		var completeinfo=queryCloud(Codex.query);
		completeinfo.complete("");}
	    return false;}
	else if (ch==32) { /* Space */
	    var qstring=target.value;
	    var completeinfo=queryCloud(Codex.query);
	    var completions=completeinfo.complete(qstring);
	    if (completions.prefix!==qstring) {
		target.value=completions.prefix;
		fdjtDOM.cancel(evt);
		setTimeout(function(){
		    Codex.UI.updateScroller("CODEXSEARCHCLOUD");},
			   100);
		return;}}
	else {
	    var completeinfo=queryCloud(Codex.query);
	    completeinfo.docomplete(target);
	    setTimeout(function(){
		Codex.UI.updateScroller("CODEXSEARCHCLOUD");},
		       100);}}
    Codex.UI.handlers.search_keyup=searchInput_keyup;

    /*
      function searchInput_onkeyup(evt){
      evt=evt||event||null;
      var kc=evt.keyCode;
      if ((kc===8)||(kc===46)) {
      if (_sbook_searchupdate) {
      clearTimeout(_sbook_searchupdate);
      _sbook_searchupdate=false;}
      var target=fdjtDOM.T(evt);
      _sbook_searchupdate=
      setTimeout(function(target){
      _sbook_searchupdate=false;
      searchUpdate(target);},
      _sbook_searchupdate_delay,target);}}
      Codex.UI.handlers.SearchInput_onkeyup=searchInput_onkeyup;
    */

    function searchUpdate(input,cloud){
	if (!(input)) input=fdjtID("CODEXSEARCHINPUT");
	if (!(cloud)) cloud=queryCloud(Codex.query);
	cloud.complete(input.value);}
    Codex.searchUpdate=searchUpdate;

    function searchInput_focus(evt){
	evt=evt||event||null;
	var input=fdjtDOM.T(evt);
	sbook_search_focus=true;
	if ((Codex.mode)&&(Codex.mode==='searchresults'))
	    CodexMode("search");
	searchUpdate(input);}
    Codex.UI.handlers.search_focus=searchInput_focus;

    function searchInput_blur(evt){
	evt=evt||event||null;
	sbook_search_focus=false;}
    Codex.UI.handlers.search_blur=searchInput_blur;

    function clearSearch(evt){
	var target=fdjtUI.T(evt||event);
	var box=fdjtDOM.getParent(target,".searchbox");
	var input=fdjtDOM.getChild(box,".searchinput");
	fdjtUI.cancel(evt);
	setQuery(Codex.empty_query);
	input.focus();}
    Codex.UI.handlers.clearSearch=clearSearch;
    
    Codex.toggleSearch=function(evt){
	evt=evt||event;
	if ((Codex.mode==="search")||
	    (Codex.mode==="searchresults"))
	    CodexMode(false);
	else {
	    CodexMode("search");
	    fdjtID("CODEXSEARCHINPUT").focus();}
	fdjtUI.cancel(evt);};
    
    /* Show search results */

    function makelocrule(target_info,cxtinfo_arg,cxtname){
	var cxtinfo=cxtinfo_arg||Codex.docinfo[(Codex.body||document.body).id];
	if (!(cxtname)) {
	    if (cxtinfo_arg) cxtname="into the section";
	    else cxtname="into the book";}
	var locrule=fdjtDOM("hr.locrule");
	var cxt_start=cxtinfo.starts_at;
	var cxt_end=cxtinfo.ends_at;
	var cxt_len=cxt_end-cxt_start;
	var target_start=target_info.starts_at-cxt_start;
	var target_len=target_info.ends_at-target_info.starts_at;
	var locstring="~"+Math.ceil(target_len/5)+ " words long ~"+
	    Math.ceil((target_start/cxt_len)*100)+"% "+cxtname;
	locrule.setA
