//
// javascript to render an items /details/ page from JSON output from the server.
// See the README-details.htm in this dir.
//
// NOTE: when "document" is undefined, that means we are doing
// "Server Side JavaScript" and there a couple minor differences...


//fixme reviews and textSection header/banner colors/fonts?


var IAD = 
{
  
SSJS_ONLY:true,   // false if our default was *NOT* SSJS for rollout...
meta:null,
css:null,
identifier:"",
loadme:"",

// defer item-specific images from being directly referenced in the main HTML
// chunk inserted into the DOM by "render()" -- instead use JS after that
// fragment is inserted to update the image.
deferImages:{},
numStarImages:0,

// for (movies) flash player, list of .flv and h.264 files (and click2play images)
flvs:    [],
mp4s:    [],
thumbs:  [],
flvnames:[],
mp4names:[],
// for (audio) flash player, lists of mp3 files:
mp3s:    [],
lengths: [],
names:   [],
// for OLPC users, we'll show the .ogv if we it instead
ogv:'',
srts:    [],


// language list for texts items; filled in (once) if needed
langList: null,

// sequence map for texts items download list; filled in (once) if needed
seqMap: null,


mediatypeItem:{
  "texts":"book",
  "audio":"audio",
  "etree":"show",
  "movies":"movie",
  "software":"software",
  "education":"educational material",
  "home":"item" // catchall for any other
},

ucverb:{
  "texts":"View the ",
  "audio":"Listen to ",
  "etree":"Listen to ",
  "movies":"View ",
  "software":"Download ",
  "education":"View ",
  "home":"Download " // catchall for any other
},

lcverbing:{
  "texts":"reading ",
  "audio":"listening to ",
  "etree":"listening to ",
  "movies":"viewing ",
  "software":"downloading ",
  "education":"viewing ",
  "home":"downloading " // catchall for any other
},


log:function(str)
{
  // Do not log if in production mode or special dev hosts
  if (typeof(location)!='undefined') {
    if (location.host.substr(0,4)!='www-') {
      return;
    }
    if (location.host.substr(0,8) == 'www-mang') {
      return;
    }
  }
  
  if (typeof(document)=='undefined')                    print(str); //SSJS
  else if (navigator.userAgent.indexOf('IE')>=0)        alert(str); //IE
  else if (navigator.userAgent.indexOf('Firefox')>=0  ||
           navigator.userAgent.indexOf('Safari') >=0)
  {
    if (typeof(console)!='undefined')                   console.log(str);
  }
},


// returns value of 0th elem of array; else
// returns '' for 0-length array; else
// reutrns node's value
pr:function(str)
{
  if (typeof(str)=='undefined')
    return '';

  if (typeof(str)=='string')
    return str;

  if (typeof(str.length)!='undefined')
  {
    if (str.length==0)
      return '';
    if (str.length>0)
      return str[0];
  }
  return str;
},


multipr:function(arr)
{
  if (typeof(arr) != 'object')
      return this.pr(arr);
  var n = arr.length;
  var lastbr = n - 2;
  var str = '';
  for (i = 0; i < n; i++)
  {
    str += arr[i];
    if (i <= lastbr)
      str += '<br/><br/>';
  }
  return str;
},


// parse a CGI arg
arg:function(theArgName)
{
  sArgs = location.search.slice(1).split('&');
  r = '';
  for (var i=0; i < sArgs.length; i++)
  {
    if (sArgs[i].slice(0,sArgs[i].indexOf('=')) == theArgName)
    {
      r = sArgs[i].slice(sArgs[i].indexOf('=')+1);
      break;
    }
  }
  return (r.length > 0 ? unescape(r).split(',') : '')
},



// transforms a file size into a good looking number with reasonable units
prettySize:function(size)
{
  // special case for skipping the unit suffix
  if ((size instanceof Array)  &&  (size[0] == 'verbatim'))
    return size[1];

  size = parseFloat(size);

  /**/ if(size<      1024)return size                         +' B'; // bytes
  else if(size<     10240)return (size/1024).toFixed(2)       +' KB';// precise K
  else if(size<   1048576)return Math.round(size/1024)        +' KB';// K
  else if(size<  10485760)return (size/1048576).toFixed(2)    +' MB';// precise M
  else if(size<1073741824)return Math.round(size/1048576)     +' MB';// M

  return (size/1073741824).toFixed(1)+' GB'; // G
},


// "extra" can be skipped or something like 'style="border:0"'
// "height" should be like "110px"
// "id" and "height" will only get used if we are "deferring images"
// (and then the height will be removed before final image insertion)
imgHelper:function(src, id, alt, height, extra)
{
  if (alt)
    alt = '['+alt+']';
  var str = '<img title="'+alt+'" alt="'+alt+'"' +
    // if we have both height and extra, and extra begins with 'style=', merge them
    // otherwise insert each (if present) singly
    ((height  &&  extra  &&  (extra.substr(0,7) == 'style="'))
     ? ' style="height:'+height+'; '+extra.substr(7)
     : (height ? ' style="height:'+height+'"' : '') +
       (extra ? ' '+extra : ''));
  
  if (this.deferImages==null)
    return str + ' src="'+src+'"/>';

  this.deferImages[id] = src;
  return str + ' id="'+id+'"/>'
},


// returns the creative commons image for the given license url
ccimage:function()
{
  var url = this.pr(this.meta.metadata.licenseurl);
  if (!url)
    return '';
  
  var node = (typeof(this.meta.creativecommons)=='undefined' ? null :
              this.meta.creativecommons);

  // if license not found in our DB, then
  // no pretty image; so just go with CC folks' recommended "generica" image
  var name='Creative Commons License';
  var  src='http://creativecommons.org/images/public/somerights20.png';
  if (node  &&  this.pr(node.license_url) == url)
  {
    name = this.pr(node.name);
    src =  this.pr(node.image_url);
  }

  return '' +
    '<div style="text-align:center; margin-top:10px;"><a rel="license" href="'+
    url + '" target="_blank">' + this.imgHelper(src, 'ccimage', name, '31px') +
    '</a></div>';
},


// adds the license if there is one
cclicense:function()
{
  var url = this.pr(this.meta.metadata.licenseurl);
  if (!url)
    return '';

  var node = (typeof(this.meta.creativecommons)=='undefined' ? null :
              this.meta.creativecommons);
  
  var val = (node  &&  this.pr(node.license_url) == url ?
             this.pr(node.name) :
             // license not found in our DB.  no pretty name to print, so just
             // print the URL AS IS
             url);

  val = '<a rel="license" title="'+val+'" href="'+url+'" target="_blank">'+
    val+'</a>';

  return '<p class="content">' +
           this.keyval2('Creative Commons license', val) + '</p>';
},

  

// returns a string that is a series of images that represents a star rating,
// given a float (rounded to nearest half-star)
stars:function(star_count)
{
  if (star_count=='')
    return '';
  
  var alt = parseInt(star_count).toPrecision(2) + ' out of 5 stars';
  
  var ret = '<span style="white-space: nowrap;">';
  for (var current_star=0; current_star < 5; current_star++)
  {
    // select the star type (whole/half/none) 
    if ((star_count - current_star) >= 0.75)
      img = "star.png";
    else if ((star_count - current_star) < 0.25)
      img = "no_star.png";
    else
      img = "half_star.png";

    this.numStarImages++;
    ret += this.imgHelper('/images/'+img, 'stars' + this.numStarImages, alt, 0);

    // put the alt text on the first star, so it only gets displayed once in
    // a text browser
    alt = '';
  }
  return ret + '</span>';
},



// finds anim GIF and returns img HTML string
thumbnail:function()
{
  var img=null;
  var anygif=null;
  var flippy=false;
  var imgPref=0;
  for (var filocation in this.meta.files)
  {
    var fi = this.meta.files[filocation];
    if (this.css=='texts'  &&  !flippy)
    {
      if (filocation.substr(filocation.lastIndexOf('_')).toLowerCase()=='_flippy.zip')
        flippy = true;
    }

    var formatLC = this.pr(fi.format).toLowerCase();
    // highest imgPref is most preferred
    // formats can be listed in any order - needn't be in order of preference
    // (note that the 3rd clause on each line is an assignment, not an equality test; the
    // idea is to save the highest imgPref seen so far, and switch to a different file
    // only if its preference is greater than the current imgPref)
    if (((formatLC == 'item image')          && (imgPref < 5) && (imgPref = 5))  ||
        ((formatLC == 'animated gif')        && (imgPref < 4) && (imgPref = 4))  ||
        ((formatLC == 'animated gif images') && (imgPref < 3) && (imgPref = 3))  ||
        ((formatLC == 'jpeg thumb')          && (imgPref < 2) && (imgPref = 2))  ||
        ((formatLC == 'jpeg')                && (imgPref < 1) && (imgPref = 1)))
    {
      img = filocation;
    }

    if (!anygif)
    {
      var suffixLC = filocation.substr(filocation.lastIndexOf('.')).toLowerCase();
      if (suffixLC=='.gif')
        anygif = filocation; //fallback plan!
    }
  }

  var width = (this.css=='texts' ? 100 : 160);
  var url = '/images/logo.jpg'; // final fallback
  if (img)
    url = 'http://'+ this.meta.server + this.meta.dir + img;
  else if (anygif)
    url = 'http://'+ this.meta.server + this.meta.dir + anygif;
  else if (this.pr(this.meta.misc.image))
    url = this.pr(this.meta.misc.image);//fixme use more elsewhere?

  if (url == '/images/logo.jpg') // NOTE: meta.misc.image can be this value,too!
    width = 0; // resorting to fallback.  don't include width...


  var target = (this.css=='movies' ? '/movies/thumbnails.php?identifier='+
                this.identifier :
                ((this.css=='texts' && flippy && (typeof(this.meta.misc.nlsitem)=='undefined')) ?
                 '/stream/'+this.identifier : ''));

  url += '?cnt=0';//xxx
  var str = ((target ? '<a onclick="return IAD.filmstrip();" href="'+target+'">' : '') +
             this.imgHelper(url, 
                            'thumbnail',
                            'item image',
                            '110px',
                            'style="margin-bottom:0.5em; border:0px;' +
                            (width ? ' width:'+width+'px;' : '') + '"') +
             (target ? '</a>' : ''));

  return str;
},


ucfirst:function(str)
{
  // http://kevin.vanzonneveld.net
  // +   original by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
  // *     example 1: ucfirst('kevin van zonneveld');
  // *     returns 1: 'Kevin van zonneveld'
  var f = str.charAt(0).toUpperCase();
  return f + str.substr(1, str.length-1);
},


// pinched from:
// http://209.85.141.104/search?q=cache:I_7LUNI_9HEJ:www.unix.org.ua/orelly/webprog/DHTML_javascript/0596004672_jvdhtmlckbk-chp-2-sect-6.html+javascript+toprecision+commas&hl=en&ct=clnk&cd=6&gl=us&client=firefox-a
formatCommas:function(numString)
{
  var re = /(-?\d+)(\d{3})/;
  while (re.test(numString))
    numString = numString.replace(re, "$1,$2");
  return numString;
},


//returns a section that contains preformated text (such as credits, notes, etc.)
//(if optional 'multi' arg is set, all instances of the element are printed, instead of just the first)
textSection:function(metaElem, multi)
{
  // make sure there is text to show
  var source = (multi ? this.multipr(this.meta.metadata[metaElem])
                : this.pr(this.meta.metadata[metaElem]));
  if (!source)
    return '';

  // make sure the heading is capitalized
  return '<h2>'+this.ucfirst(metaElem)+'</h2><p class="content">' +
  source.replace(/\n/g, '<br/>') + '</p>';
},


downloadEditable:function(shortFile)
{
  if (!this.pr(this.meta.user.editableFiles)  ||
      (!shortFile.match(/\.txt$/i)  &&  !shortFile.match(/\.md5$/i)))
  {
    return '';
  }


  // NOTE: safe to use escape() and *not* encodeURIComponent() here because
  // all filenames should be ASCII chars only
  // (encodeURIComponent() is needed for multibyte UTF8 chars, etc.)
  return ' [<a href="/services/edit_file.php?identifier='+this.identifier+
    '&file='+ escape(shortFile) +'">edit</a>]';
},


downloadTable:function(stream_only)
{
  if (this.css=='movies'  &&  stream_only)
    return '';
  
  
  // options for where we place files of different types
  // we compare the last "word" of each <format> tag for every file of the item
  // and classify each file according to the "value" below
  // (with the exception of the formats in "multiWordFormats" -- in that case
  //  we compare the entire format)
  var formatPlacement = {"AIFF"             :"Audio Files",
                         "MP3"              :"Audio Files",
                         "Shorten"          :"Audio Files",
                         "Flac"             :"Audio Files",
                         "Vorbis"           :"Audio Files",
                         "Flac"             :"Audio Files",
                         "WAVE"             :"Audio Files",
                         "Real Audio"       :"Audio Files",
                         "Windows Media Audio" : "Audio Files",
                         "Advanced Audio Coding" : "Audio Files",
                         
                         'MPEG1'            :'Movie Files',
                         'MPEG2'            :'Movie Files',
                         'MPEG4'            :'Movie Files',
                         'QuickTime'        :'Movie Files',
                         'Motion JPEG'      :'Movie Files',
                         'Windows Media'    :'Movie Files',
                         'Cinepack'         :'Movie Files',
                         'DivX'             :'Movie Files',
                         'Real Media'       :'Movie Files',
                         'DV Video'         :'Movie Files',
                         'Intel Video'      :'Movie Files',
                         'Ogg Theora'       :'Movie Files',
                         'Ogg Video'        :'Movie Files',
                         '3GP'              :'Movie Files',

                         'ISO Image'        :'Disc Images',
                         
                         'Thumbnail'        :'Thumbnails',
                         'JPEG Thumb'       :'Thumbnails',
                         
                         'JPEG'             :'Image Files',
                         'TIFF'             :'Image Files',
                         'Adobe Illustrator' :'Image Files',

                         "Checksums"        :"Information",
                         "Metadata"         :"Information",
                         "FingerPrint"      :'Information',
                         "ZIP"              :"Whole Item",
                         "M3U"              :"Whole Item"};

  var multiWordFormats = {'Motion JPEG':1,
                          'Real Media':1,
                          'DV Video':1,
                          'Intel Video':1,
                          'ISO Image':1,
                          'Ogg Theora':1,
                          'Ogg Video':1,
                          'Windows Media':1,
                          'JPEG Thumb':1,
                          'Real Audio':1,
                          'Windows Media Audio':1,
                          'Advanced Audio Coding':1,
                          'Flash Authoring':1,
                          'Adobe Illustrator':1};
      
  var streaming = {"M3U":1};  

  // (since movies can now show *both* audio and movie files sections,
  //  make sure we show movie files *first* for movies items)
  var tableOrder = [(this.css=='movies'?"Movie Files":"Whole Item"),
                    "Disc Images", // NOTE: only appears for movies
                    "Audio Files",
                    (this.css=='movies'?"Whole Item":"Movie Files"),
                    "Image Files",
                    "Thumbnails",
                    "Information",
                    "Other Files"]; //NOTE: indiv. movies items can elect to show
     
  
  // order files by format
  // hashmap: key is format string; value is array of filename strings
  var fileFormats = {};

  for (var filocation in this.meta.files)
  {
    var metaArray = this.meta.files[filocation];

    // omit "pure-ftpd" partial upload "turd files"...
    if (filocation.substr(0,17)=='/.pureftpd-upload')
      continue;

    var format = this.pr(metaArray.format);
    if (typeof(fileFormats[format])=='undefined')
      fileFormats[format] = [];
    fileFormats[format].push(filocation);
  }
  //this.log(fileFormats);
  

  var availableFormats = {}; // "hashmap" where each value is an array
  var tables = {}; // hashmap of a hashmap of a hashmap!

  for (var format in fileFormats)
  {
    var filesArray = fileFormats[format];
    
    var wordsArray = format.split(' ');
    
    for (var ii=0, file; file = filesArray[ii]; ii++)
    {
      var derivFile = file.substr(1); // remove lead "/" char
      var sourceFile =  this.pr(this.meta.files[file].original);

      if (sourceFile)
      {
        for (var findSrc=0; findSrc < 10; findSrc++) // avoid infin loops 8-)
        {
          var tmp = this.meta.files['/'+sourceFile];
          if (!tmp)
            break;
          tmp = this.pr(tmp.original);
          if (!tmp)
            break;
          sourceFile = tmp;
        }
      }
      else
      {
        sourceFile = derivFile;
      }
      //this.log(sourceFile+' ==> '+derivFile);
      
      var key = (typeof(multiWordFormats[format])!='undefined' ?
                 format : wordsArray[wordsArray.length-1]);
      var formatType = (typeof(formatPlacement[key])!='undefined' ?
                        formatPlacement[key] : 'Other Files');

      if (typeof(tables[formatType])=='undefined')
        tables[formatType] = {};
      if (typeof(tables[formatType][sourceFile])=='undefined')
        tables[formatType][sourceFile] = {};
      tables[formatType][sourceFile][format] = derivFile;

      if (typeof(availableFormats[formatType])=='undefined')
        availableFormats[formatType] = {};
      availableFormats[formatType][format] = 1;
    }
  }
  //this.log(availableFormats);
  //this.log(tables);
  
  


  var ret = '<h2>Individual Files</h2><div style="text-align:center; margin:0px auto 0px auto; padding:0.1em">'

  for (var itt=0, tableType; tableType=tableOrder[itt]; itt++)
  {
    // Only show "Movie Files" (or "Audio Files") for movies pages and omit the
    // rest! (mar2009 tracey added "Audio Files" since more items are offering
    // audio-only soundtracks, etc... eg /details/Sita_Sings_the_Blues )
    // (june2009 tracey added "Disc Images" for movies)
    if (tableType=='Disc Images')
    {
      if (this.css!='movies')
        continue;
      //this.log(tables[tableType]);
    }
    else if (this.css=='movies'  &&  tableType!='Movie Files'  &&
             tableType!='Audio Files')
    {
      if (tableType=='Other Files'  &&  !this.pr('show_other_files'))
        continue;
    }

    if (stream_only  &&  tableType!='Information'  &&  tableType!='Other Files')
      continue; // only show information, nothing they can download


    var sFilesArray = tables[tableType];
    if (typeof(sFilesArray)=='undefined')
      continue;


    ret+= '<table id="ff'+itt+'" class="fileFormats"><tr>';
    ret+= '<td class="ttlHeader">'+tableType+'</td>';
    
    if (tableType=='Whole Item'  ||  tableType=='Information')
    {
      ret+= "<td>Format</td><td>Size</td>";
      var rown=0;
      for (var sourceFile in tables[tableType])
      {
        var dFilesArray = tables[tableType][sourceFile];

        for (var derivFormat in dFilesArray)
        {
          var derivFile = dFilesArray[derivFormat];

          var tmp = this.meta.files['/'+derivFile];

          var display = null;
          if (typeof(tmp)!='undefined'  &&  tableType=='Whole Item')
            display = this.pr(tmp.title);
          if (!display)
            display = derivFile;


          var display2 = derivFormat;
          if (!display2)
            display2 = derivFile;
          

          ret +=
            '<tr class="'+(rown%2?'eve':'odd')+'"><td class="ttl">'+display +
            this.downloadEditable(derivFile) +
            '</td><td class="ttl">'+display2+'</td><td>';
          rown++;
          

          display = '??B';
          if (typeof(tmp)!='undefined')
          {
            var size = tmp.size;
            if (size)
              display = this.prettySize(size);
          }
          
          var wordsArray = derivFormat.split(' ');
          if (typeof(streaming[wordsArray[wordsArray.length-1]])!='undefined')
            display = "Stream";
          
          ret += '<a href="/download/'+this.identifier+'/'+derivFile+'">'+
            display+'</a>'+
            "</td></tr>";
        }
      }
    }
    else
    {
      for (var format in availableFormats[tableType])
        ret += "<td>"+format+"</td>";
      
      ret += "</tr>";

      // sort all the "keys" (source filenames)
      var sortedSourceFiles = [];
      for (var sourceFile in sFilesArray)
        sortedSourceFiles.push(sourceFile);
      sortedSourceFiles = sortedSourceFiles.sort();
      
      for (var i=0, sourceFile; sourceFile=sortedSourceFiles[i]; i++)
      {
        var derivArray = sFilesArray[sourceFile];
        
        ret += '<tr class="'+(i%2?'eve':'odd')+'">';

        var tmp = this.meta.files['/'+sourceFile];
        var display = null;
        if (typeof(tmp)!='undefined')
          display = this.pr(tmp.title);
        if (!display)
          display = sourceFile;

        ret += '<td class="ttl">'+display+'</td>';
        for (var format in availableFormats[tableType])
        {
          var derivFile = derivArray[format];
          if (derivFile)
          {
            ret += "<td>";

            var tmp = this.meta.files['/'+derivFile];
            display = '??B';
            if (typeof(tmp)!='undefined')
            {
              var size = tmp.size;
              if (size)
                display = this.prettySize(size);
            }
            
            
            ret +=
              '<a href="/download/'+this.identifier+'/'+derivFile+'">'+
              display+'</a>' + this.downloadEditable(derivFile) + '</td>';
          }
          else
          {
            ret+= "<td> </td>";
          }
        }
        ret+= "</tr>";
      }
    }
    
    ret += "</table>";
  }
  return ret + '</div>';
},


textsPOD:function()
{
    // get pointers to the several files we'll need (if they're present)
    var abbyy = '';
    var cover = '';
    var imgzip = '';
    var bwpdf = '';
    var colorpdf = '';
    var filesObj = this.meta.files;
    for (fname in filesObj)
    {
       var format = filesObj[fname].format;
       switch (format)
       {
         case 'Abbyy GZ':
           abbyy = fname; break;
         case 'Abbyy ZIP':
           if (!abbyy) abbyy = fname; break; // Abbyy GZ takes priority
         case 'Abbyy XML':
           if (!abbyy) abbyy = fname; break; // Abbyy GZ takes priority
         case 'Book Cover Image':
           cover = fname; break;
         case 'Single Page Processed JP2 ZIP':
           imgzip = fname; break;
         case 'Single Page Original JPEG ZIP':
           if (!imgzip) imgzip = fname; break; // JP2 takes priority
         case 'Grayscale LuraTech PDF':
           bwpdf = fname; break;
         case 'Standard LuraTech PDF':
         case 'Image Container PDF':
         case 'PDF':
           colorpdf = fname; break;
       }
    }
    var host = location.hostname;
    var locDLprefix = '/download/' + this.identifier;
    var remDLprefix = 'http://' + host + locDLprefix;
    var ret = '';

    if (abbyy && imgzip)
    {
      // do cover image section
      // scale the image on the server, so we're not sending the full-size jpg
      var coverimgurl = 'http://' + this.meta.server + '/jpegscale.php?file=' +
                        this.meta.dir + cover + '&width=166';
      ret += '<p style="padding: 0 0 5px;" class="content">';
      ret += (cover ? this.imgHelper(coverimgurl, 'cover', 'selected image', '200px', 'width="166"')
                    : this.imgHelper('/images/notfound.jpg', 'cover', 'none selected', '110px', 'width="160"'))
      ret += '<b>Front cover image</b><br/>';
      ret += '<a href="/texts/book-cover.php?identifier=' + this.identifier + '&amp;abbyy=' +
             abbyy.substr(abbyy.lastIndexOf('/')+1) + '">Select/change</a><br/>';
      if (cover)
        ret += '<a href="' + locDLprefix + cover + '">Preview</a><br/>';
      ret += '</p>';
    }

    if (bwpdf)
    {
      // show "Order printed copy" link
      ret += '<p style="border-top:1px dashed #93092D; padding: 5px 0 6px;" class="content">';
      var coverurl = 'http://' + host + '/texts/book-cover.php?book_block=' + remDLprefix + bwpdf +
                     '%26identifier=' + this.identifier + '%26rotate=1%26download-cover=24';
      if (host.substr(0, 8) == 'www-hank') // draw proof lines on cover if on www-hank
        coverurl += '%26proof=1';
      ret += '<a href="http://ebm.archive.org/printbook.php?identifier=' + this.identifier +
             '&amp;book_block=' + remDLprefix + bwpdf + '&amp;submitter=' + this.meta.user.screenname +
             '&amp;book_cover=' + coverurl + '">Order printed copy</a><br/>';
      ret += '</p>';
    }

    if (bwpdf || colorpdf)
    {
      // do download section
      ret += '<p style="border-top:1px dashed #93092D; padding: 5px 0 0;" class="content">';
      ret += '<b>Download for printing</b><br/>';
      if (bwpdf)
        ret += '<a href="/texts/book-cover.php?identifier=' + this.identifier + '&amp;book_block=' +
               locDLprefix + bwpdf + '&amp;download-cover=">full cover</a> (front and back)<br/>';
      if (bwpdf)
        ret += '<a href="' + locDLprefix + bwpdf + '">b/w</a> book block<br/>';
      if (colorpdf)
        ret+= '<a href="' + locDLprefix + colorpdf + '">color</a> book block<br/>';
      ret += '</p>';
    }

    return ret;
},

// returns array of two values:  the text to display, and the search query to link it to
textsLanguage:function(elem, name)
{
  if (!this.langList) // big'n, create only first time through (this function may be called repeatedly)
    this.langList = 
  {
 // short (marc) and long versions of language names
 // if a 'language' metadata element matches either version, the long form will be displayed on the details
 // page, linked to a search engine query on an OR of both forms
 // (if no match is found, the metadata value itself is displayed, linked to a query on just that value)
'abk' : 'Abkhaz',
'ady' : 'Adyghe',
'afr' : 'Afrikaans',
'akk' : 'Akkadian',
'alb' : 'Albanian',
'ale' : 'Aleut',
'alg' : 'Algonquian',
'amh' : 'Amharic',
'ang' : 'Old English',
'ara' : 'Arabic',
'arg' : 'Aragonese',
'arm' : 'Armenian',
'asm' : 'Assamese',
'aym' : 'Aymara',
'aze' : 'Azerbaijani',
'bak' : 'Bashkir',
'bal' : 'Baluchi',
'baq' : 'Basque',
'bel' : 'Belarusian',
'bem' : 'Bemba',
'ben' : 'Bengali',
'ber' : 'Berber',
'bnt' : 'Bantu',
'bre' : 'Breton',
'bua' : 'Buryat',
'bul' : 'Bulgarian',
'car' : 'Central American Indian',
'cat' : 'Catalan',
'cau' : 'Caucasian',
'ceb' : 'Cebuano',
'cha' : 'Chamorro',
'che' : 'Chechen',
'chi' : 'Chinese',
'chm' : 'Mari',
'chn' : 'Chinook jargon',
'cho' : 'Choctaw',
'chp' : 'Chipewyan',
'chu' : 'Church Slavic',
'chv' : 'Chuvash',
'cop' : 'Coptic',
'cor' : 'Cornish',
'cos' : 'Corsican',
'cpe' : 'Creoles and Pidgins, English-based',
'cpf' : 'Creoles and Pidgins, French-based',
'cpp' : 'Creoles and Pidgins, Portuguese-based',
'cre' : 'Cree',
'crh' : 'Crimean Tatar',
'crp' : 'Creoles and Pidgins',
'cze' : 'Czech',
'dan' : 'Danish',
'dar' : 'Dargwa',
'del' : 'Delaware',
'dum' : 'Middle Dutch',
'dut' : 'Dutch',
'eng' : 'English',
'enm' : 'Middle English',
'epo' : 'Esperanto',
'esk' : 'Eskimo',
'esp' : 'Esperanto',
'est' : 'Estonian',
'eth' : 'Ethiopic',
'fao' : 'Faroese',
'far' : 'Faroese',
'fij' : 'Fijian',
'fin' : 'Finnish',
'fre' : 'French',
'fri' : 'Frisian',
'frm' : 'Middle French',
'fro' : 'Old French',
'frr' : 'North Frisian',
'fry' : 'Frisian',
'fur' : 'Friulian',
'gaa' : 'Gã',
'gae' : 'Scottish Gaelic',
'gag' : 'Galician',
'gem' : 'Germanic',
'geo' : 'Georgian',
'ger' : 'German',
'gez' : 'Ethiopic',
'gla' : 'GaelicScottish',
'gle' : 'Irish',
'glg' : 'Galician',
'gmh' : 'Middle High German',
'goh' : 'Old German',
'grb' : 'Grebo',
'grc' : 'Ancient Greek',
'gre' : 'Greek',
'grn' : 'Guarani',
'gsw' : 'Swiss German',
'gua' : 'Guarani',
'gwi' : 'Gwichin',
'hai' : 'Haida',
'hau' : 'Hausa',
'haw' : 'Hawaiian',
'heb' : 'Hebrew',
'hin' : 'Hindi',
'hsb' : 'Upper Sorbian',
'hun' : 'Hungarian',
'ice' : 'Icelandic',
'ido' : 'Ido',
'ile' : 'Interlingue',
'ilo' : 'Iloko',
'ina' : 'Interlingua',
'ind' : 'Indonesian',
'inh' : 'Ingush',
'int' : 'Interlingua',
'ira' : 'Iranian',
'iri' : 'Irish',
'iro' : 'Iroquoian',
'ita' : 'Italian',
'iku' : 'Inuktitut',
'jpn' : 'Japanese',
'jrb' : 'Judeo-Arabic',
'kaa' : 'Karakalpak',
'kal' : 'Kalatdlisut',
'kan' : 'Kannada',
'kar' : 'Karen',
'kaz' : 'Kazakh',
'kbd' : 'Kabardian',
'kha' : 'Khasi',
'kik' : 'Kikuyu',
'kir' : 'Kirgiz',
'kon' : 'Kongo',
'kor' : 'Korean',
'kpe' : 'Kpelle',
'kro' : 'Kru',
'kum' : 'Kumyk',
'kur' : 'Kurdish',
'lad' : 'Ladino',
'lap' : 'Lappish',
'lat' : 'Latin',
'lav' : 'Latvian',
'lez' : 'Lezgin',
'lit' : 'Lithuanian',
'lua' : 'Luba',
'lub' : 'Luba',
'lug' : 'Ganda',
'mac' : 'Macedonian',
'mah' : 'Marshallese',
'mao' : 'Maori',
'map' : 'Austronesian',
'mar' : 'Marathi',
'may' : 'Malay',
'mga' : 'Middle Irish',
'mic' : 'Micmac',
'min' : 'Minankabaw',
'mis' : 'Miscellaneous languages',
'mla' : 'Malagasy',
'mlg' : 'Malagasy',
'mlt' : 'Maltese',
'moh' : 'Mohawk',
'mol' : 'Moldavian',
'mon' : 'Mongol',
'mul' : 'Multiple',
'myn' : 'Maya',
'nah' : 'Nahuatl',
'nai' : 'North American Indian',
'nap' : 'Neapolitan',
'nds' : 'Low German',
'new' : 'Newari',
'nic' : 'Niger-Kordofanian',
'nno' : 'Norwegian (Nynorsk)',
'nob' : 'Norwegian (Bokmål)',
'nog' : 'Nogay',
'non' : 'Old Norse',
'nor' : 'Norwegian',
'nya' : 'Nyanja',
'oci' : 'Occitan',
'oji' : 'Ojibwa',
'oss' : 'Ossetic',
'ota' : 'Turkish',
'oto' : 'Otomian',
'pag' : 'Pangasinan',
'pal' : 'Pahlavi',
'pam' : 'Pampanga',
'pap' : 'Papiamento',
'per' : 'Persian',
'phi' : 'Philippine',
'pli' : 'Pali',
'pol' : 'Polish',
'por' : 'Portuguese',
'pra' : 'Prakrit',
'pro' : 'Provencal',
'que' : 'Quechua',
'roa' : 'Romance',
'roh' : 'RhaetoRomanic',
'rom' : 'Romany',
'rum' : 'Romanian',
'run' : 'Rundi',
'rus' : 'Russian',
'sah' : 'Yakut',
'sam' : 'Samaritan Aramaic',
'san' : 'Sanskrit',
'sao' : 'Samoan',
'sat' : 'Santali',
'scc' : 'Serbian',
'scr' : 'Croatian',
'sel' : 'Selkup',
'sem' : 'Semitic',
'sga' : 'Old Irish',
'sho' : 'Shona',
'sio' : 'Siouan',
'sit' : 'Sino-Tibetan',
'sla' : 'Slavic',
'slo' : 'Slovak',
'slv' : 'Slovenian',
'smi' : 'Sami',
'smo' : 'Samoan',
'sms' : 'Skolt Sami',
'sna' : 'Shona',
'snh' : 'Sinhalese',
'som' : 'Somali',
'sot' : 'Sotho',
'spa' : 'Spanish',
'sso' : 'Sotho',
'ssw' : 'Swazi',
'sun' : 'Sunda',
'sux' : 'Sumerian',
'swa' : 'Swahili',
'swe' : 'Swedish',
'swz' : 'Swazi',
'syr' : 'Modern Syriac',
'tag' : 'Tagalog',
'tah' : 'Tahitian',
'taj' : 'Tajik',
'tam' : 'Tamil',
'tar' : 'Tatar',
'tat' : 'Tatar',
'tgk' : 'Tajik',
'tel' : 'Telugu',
'tgl' : 'Tagalog',
'tha' : 'Thai',
'tib' : 'Tibetan',
'tig' : 'Tigre',
'tlh' : 'Klingon',
'tog' : 'Tonga',
'ton' : 'Tongan',
'tpi' : 'Tok Pisin',
'tsi' : 'Tsimshian',
'tsn' : 'Tswana',
'tsw' : 'Tswana',
'tuk' : 'Turkmen',
'tur' : 'Turkish',
'tut' : 'Altaic',
'tyv' : 'Tuvin',
'udm' : 'Udmurt',
'ukr' : 'Ukrainian',
'urd' : 'Urdu',
'uzb' : 'Uzbek',
'wel' : 'Welsh',
'wen' : 'Sorbian',
'wol' : 'Wolof',
'xal' : 'Oirat',
'xho' : 'Xhosa',
'yid' : 'Yiddish',
'zap' : 'Zapotec',
'zul' : 'Zulu',
'zxx' : 'No linguistic content'
  };
  var nameLC = name.toLowerCase();
  // try the quick and easy way first (should work for most books)
  full = this.langList[nameLC];
  if (typeof(full) != 'undefined')
    return [full, '('+elem+':'+nameLC+' OR '+elem+':"'+full+'")'];
  // oh well, no match on marc, got to do it the slow way
  for (marc in this.langList)
  {
    var full = this.langList[marc];
    if (full.toLowerCase() != nameLC)
      continue;
    return [full, '('+elem+':'+marc+' OR '+elem+':"'+full+'")'];
  }
  // not found at all
  return [name, elem+':"'+name+'"'];
},

// in sponsor and contributor matching lists, any high-byte/multi-byte
// characters should be given as Unicode escape sequences rather than directly
// as UTF-8; client-side js handles the UTF-8 fine, but server-side js fails
textsSponsor:function(sponsor)
{
  if (!sponsor)
    return '';
  var linkList =
  {
// keys must be given here in lower case (the actual metadata can be in any case to match)
// values given here must match the case of the collection details page url
'allen county public library genealogy center' : 'Allen_County_Public_Library',
'baynet'                                  : 'baynet',
'boston college libraries'                : 'Boston_College_Library',
'boston library consortium member libraries' : 'blc',
'boston public library'                   : 'bostonpubliclibrary',
'brandeis university libraries'           : 'Brandeis_University',
'brigham young university'                : 'brigham_young_university',
'c. v. starr east asian library, university of california, berkeley' : 'starr',
'california department of fish and game'  : 'CaliforniaFishandGame',
'china-america digital academic library (cadal)' : 'cadal',
'duke university libraries'               : 'duke_libraries',
'ensuring democracy through digital access, a north carolina lsta-funded grant project' : 'ncgovdocs',
'family search'                           : 'genealogy',
'google'                                  : 'googlebooks',
'gorgias press'                           : 'gorgiaspress',
'harvard university, museum of comparative zoology, ernst mayr library' : 'Harvard_University',
'havergal college'                        : 'havergal',
'internet archive'                        : 'internet_archive_books',
'legislative assembly of ontario'         : 'ontla',
'leo baeck institute archives'            : 'LeoBaeckInstitute',
'lyrasis members and sloan foundation'    : 'lyrasis',
'mblwhoi library'                         : 'MBLWHOI',
'montana state library'                   : 'MontanaStateLibrary',
'msn'                                     : 'msn_books', // usage="details/msn_books"
'national institute for newman studies'   : 'national_institute_for_newman_studies',
'national yiddish book center'            : 'nationalyiddishbookcenter',
'natural history museum library, london'  : 'nhml_london',
'private collection of the works of the great school of natural science (gsns)' : 'greatschoolofnaturalscience',
'sloan foundation'                        : 'sloan',
'smithsonian'                             : 'smithsonian_books',
'st. andrew\'s college'                   : 'standrewscollegearchives',
'the long now foundation'                 : 'longnow',
'the luesther t mertz library, the new york botanical garden' : 'NY_Botanical_Garden',
'tufts university and the national science foundation' : 'tufts',
'umass lowell libraries'                  : 'umasslowell',
'universidad francisco marroqu\u00EDn'    : 'guatemala', // Unicode U+00ED is i-acute (í)
'university of alberta libraries'         : 'university_of_alberta_libraries',
'university of california libraries'      : 'university_of_california_libraries',
'university of florida, george a. smathers libraries' : 'univ_florida_smathers',
'university of guelph'                    : 'university_of_guelph',
'university of illinois urbana-champaign' : 'university_of_illinois_urbana-champaign',
'university of massachusetts, boston'     : 'umass_boston',
'university of new hampshire library'     : 'University_of_New_Hampshire_Library',
'university of north carolina at chapel hill' : 'unclibraries',
'university of toronto'                   : 'university_of_toronto',
'wellesley college library'               : 'Wellesley_College_Library',
'yahoo!'                                  : 'yahoo_books'
  };
  var usageList =
  {
// each key must match (case-sensitively) a key in linkList
// value must match (case-sensitively) the details page url
// 'msn' : 'msn_books'
  };
  return this.textsLookup('Digitizing sponsor', sponsor, linkList, null, usageList);
},


textsContributor:function(contributor, sponsor) // need sponsor, as it can affect contributor label
{
  if (!contributor)
    return '';
  var linkList =
  {
// keys must be given here in lower case (the actual metadata can be in any case to match)
// values given here must match the case of the collection details page url
'allen county public library genealogy center' : 'Allen_County_Public_Library',
'biblioteca ludwig von mises, universidad francisco marroqu\u00EDn' : 'guatemala', // Unicode U+00ED is i-acute (í)
'bloomsburg university, harvey a. andruss library' : 'andrusslibrary',
'boston college libraries'                : 'Boston_College_Library',
'boston public library'                   : 'bostonpubliclibrary',
'brandeis university libraries'           : 'Brandeis_University',
'c. v. starr east asian library, university of california, berkeley' : 'starr',
'california academy of sciences'          : 'calacademy',
'california department of fish and game'  : 'CaliforniaFishandGame',
'canadiana.org'                           : 'canadiana_org',
'carnegie library of pittsburgh'          : 'carnegie_lib_pittsburgh',
'columbia university libraries'           : 'ColumbiaUniversityLibraries',
'cornell university library'              : 'cornell',
'duke university libraries'               : 'duke_libraries',
'elizabethtown college, the high library' : 'elizabethtowncollege',
'falvey memorial library, villanova university' : 'villanova_university',
'family history library'                  : 'family_history_library',
'gettysburg college, musselman library, special collections' : 'gettysburgcollege',
'goucher college'                         : 'goucher_college',
'harold b. lee library'                   : 'brigham_young_university',
'harvard university'                      : 'Harvard_University',
'harvard university, museum of comparative zoology, ernst mayr library' : 'Harvard_University',
'havergal college library'                : 'havergal',
'independence seaport museum, j. welles henderson archives and library' : 'independence_seaport_museum',
'institute for advanced study'            : 'instituteadvancedstudy',
'internet archive'                        : 'internet_archive_books',
'john adams library at the boston public library' : 'johnadamsBPL',
'johns hopkins university'                : 'Johns_Hopkins_University', // contains='Johns Hopkins University',
'lancaster county historical society'     : 'lancaster_county',
'legislative assembly of ontario'         : 'ontla',
'leo baeck institute archives'            : 'LeoBaeckInstitute',
'library and archives canada'             : 'library_and_archives_canada',
'lycoming college, snowden library'       : 'lycoming_college',
'mblwhoi library'                         : 'MBLWHOI',
'mcmaster university'                     : 'memorial_university',
'memorial - university of newfoundland'   : 'memorialuniversitynewfoundland',
'montana state library'                   : 'MontanaStateLibrary',
'national library of australia'           : 'national_library_of_australia',
'national yiddish book center'            : 'nationalyiddishbookcenter',
'natural history museum library, london'  : 'nhml_london',
'new jersey state library'                : 'njstatelibrary',
'new york public library'                 : 'newyorkpubliclibrary',
'north georgia college & state university, library technology center' : 'northgeorgia',
'o\'reilly'                               : 'oreilly_books',
'penn state university'                   : 'penn_state_univ',
'pennsylvania college of technology, roger & peggy madigan library' : 'madiganlibrary',
'philadelphia museum of art, library'     : 'philadelphiamuseumofart',
'prelinger library'                       : 'prelinger_library',
'presidio trust library'                  : 'PresidioTrustLibrary',
'princeton theological seminary library'  : 'Princeton',
'private collection of the works of the great school of natural science (gsns)' : 'greatschoolofnaturalscience',
'project gutenberg'                       : 'gutenberg',
'research library, the getty research institute' : 'getty',
'ryerson university'                      : 'ryerson_university',
'saint mary\'s college of california'     : 'saint_marys_college', // contains='Saint Mary\'s College'
'smithsonian institution libraries'       : 'smithsonian_books',
'smithsonian'                             : 'smithsonian_books',
'st. andrew\'s college archives'          : 'standrewscollegearchives',
'st. mary\'s college of california'       : 'saint_marys_college', // contains='St. Mary\'s College'
// full match on next one avoids conflict with the "contains" on the previous one
'st. mary\'s college of maryland library' : 'saint_marys_college_maryland',
'svk'                                     : 'svk_library',
'the bancroft library'                    : 'bancroft',
'the british library'                     : 'the_british_library',
'the curtis institute of music'           : 'curtisinstituteofmusic',
'the library of congress'                 : 'library_of_congress', // contains='Library of Congress'
'the long now foundation'                 : 'longnow',
'the luesther t mertz library, the new york botanical garden' : 'NY_Botanical_Garden',
'the university of scranton weinberg memorial library' : 'university_scranton',
'toronto public library : toronto reference library' : 'toronto_public_library', // contains='Toronto Public Library'
'umass lowell libraries'                  : 'umasslowell',
'universal digital library'               : 'universallibrary',
'university library, university of north carolina at chapel hill' : 'unclibraries',
'university of california berkeley'       : 'university_of_california_libraries',
'university of california libraries'      : 'university_of_california_libraries',
'university of california, berkeley'      : 'university_of_california_libraries',
'university of chicago'                   : 'uchicago',
'university of florida, george a. smathers libraries' : 'univ_florida_smathers',
'university of illinois urbana-champaign' : 'university_of_illinois_urbana-champaign',
'university of maryland, baltimore'       : 'university_maryland_baltimore',
'university of maryland, college park'    : 'university_maryland_cp',
'university of maryland school of law, thurgood marshall law library' : 'thurgoodmarshalllawlibrary',
'university of massachusetts, boston'     : 'umass_boston',
'university of michigan'                  : 'michigan_books',
'university of new hampshire library'     : 'University_of_New_Hampshire_Library',
'university of ottawa'                    : 'university_of_ottawa',
'university of pittsburgh library system' : 'university_pittsburgh',
'university of pennsylvania libraries'    : 'upenn',
'university of toronto'                   : 'university_of_toronto', // contains="University of Toronto"
'wellesley college library'               : 'Wellesley_College_Library',
'west virginia university libraries'      : 'west_virginia_university',
'west chester university of pennsylvania, library services' : 'westchester_u',
'\u6D59\u6C5F\u5927\u5B66\u56FE\u4E66\u9986' : 'zhejiang' // Unicode for '浙江大学图书馆'
  };
  var looseMatchList =
  [
// if first of pair is *contained* in the Contributor metadata, use the second to do an exact lookup in
// the linkList above
// (first can be in any case, second must be lower case)
['Johns Hopkins University', 'johns hopkins university'],
['Library of Congress', 'the library of congress'],
['Saint Mary\'s College', 'saint mary\'s college of california'],
['St. Mary\'s College', 'st. mary\'s college of california'],
['Toronto Public Library', 'toronto public library : toronto reference library'],
['University of Toronto', 'university of toronto']
  ];
  var usageList =
  {
// each key must match (case-sensitively) a key in linkList
// value must match (case-sensitively) the details page url
'cornell university library' : 'cornell'
  };
  var label = ((sponsor == 'Google') ? 'Book from the collections of' : 'Book contributor');
  return this.textsLookup(label, contributor, linkList, looseMatchList, usageList);
},


textsLookup:function(label, name, linkList, looseMatchList, usageList) // looseML and usageL are optional
{
  var link = linkList[name.toLowerCase()];
  // if not found, and looseMatchList provided, try looking there
  if ((typeof(link) == 'undefined')  &&  looseMatchList)
  {
    var lm = this.looseMatch(name, looseMatchList); // [match-found?, name-if-found]
    if (lm[0])
    {
      name = lm[1];
      link = linkList[name];
    }
  }
  // if still not found, just use the name as is; otherwise linkify
  var str = this.keyval2(label, ((typeof(link) == 'undefined')
                                 ? name
                                 : this.linkify(name, '/details/' + link)));

  // now check for Usage Rights link (name is already normalized, if necessary, by looseMatch)
  if (usageList)
  {
    var ulink = usageList[name.toLowerCase()];
    if (typeof(ulink) != 'undefined')
      str += this.keyval2(((label == 'Digitizing sponsor') ? 'Sponsor' : 'Contributor') + ' usage rights',
                          this.linkify('See terms', '/details/' + ulink));
  }

  return str;
},


looseMatch:function(name, linkList, looseMatchList)
{
  var haystack = name.toLowerCase();
  for (entry in looseMatchList)
  {
    var needle = looseMatchList[entry][0].toLowerCase();
    if (haystack.indexOf(needle) >= 0)
      // found it, return the normalized name
      return [ true, looseMatchList[entry][1] ];
  }
  // not found
  return [false];
},


reviews:function(reviewsL)
{
  if (reviewsL.length==0)
    return '';

  var str = '';
  var rsorted = reviewsL.sort(IAD.sortReviews);
  for (var i=0, rev; rev = rsorted[i]; i++)
  {
    var rever = this.pr(rev.reviewer);
    if (rever)
    {
      rever = '<a href="/search.php?query=reviewer:%22' +
        encodeURIComponent(rever)+'%22">'+rever+'</a>';
    }
    
    str += '<p style="margin:3px; padding:4px; border:1px solid #ccc;"><b>Reviewer:</b> ' + rever + ' - ' +
      this.stars(this.pr(rev.stars)) + ' - ' +
      this.toPrettyDate(this.pr(rev.reviewdate)) + '<br/>' +
      '<b>Subject:</b> ' + this.pr(rev.reviewtitle) + '<br/>' +
      this.pr(rev.reviewbody).replace(/\n/g, '<br/>') + '</p>';
  }

  return str;
},


textsSelMetadata:function(metadata)
{
  var ignoreFields = {
    // exclude these elements that already appear in the overview section at the top of the page
    "title"       : 1,
    "volume"      : 1,
    "date"        : 1,
    "creator"     : 1,
    "subject"     : 1,
    "publisher"   : 1,
    "year"        : 1,
    "possible-copyright-status" : 1,
    "language"    : 1,
    "call_number" : 1,
    "sponsor"     : 1,
    "contributor" : 1,
    "collection"  : 1,
    "notes"       : 1,
    "scanfactors" : 1,
    "paginated"   : 1, //present implicitly; if 'true', pdf section will appear
    "description" : 1,
    // and these others that are deemed inessential to display on the details page
    "addeddate"   : 1,
    "publicdate"  : 1,
    "updatedate"  : 1,
    "uploader"    : 1,
    "updater"     : 1,
    "curation"    : 1,
    "repub_state" : 1,
    "prevtask"    : 1,
    "nre"         : 1,
    "admincmd"    : 1,
    "adminuser"   : 1,
    "admintime"   : 1};
  var hideEmail = {
     // obscure the email addresses in these elements
     // if no delimiter is specified, will replace everything after the first '@' with '...'
     // with a delimiter, replaces only the text between the '@' and the delimiter
    // these are excluded anyway
    //"curation" : '[',
    //"uploader" : '',
    //"updater"  : '',
    "operator" : ''
  };
  var str = '<h2>Selected metadata</h2><table>';
  for (elem in metadata)
  {
    // skip the output if this element is listed in ignoreFields
    if (ignoreFields[elem] === 1)
      continue;

    // obscure the email address if this element is listed in hideEmail (and the metadata value contains '@')
    if (typeof(hideEmail[elem]) != 'undefined')
    {
      // is in hideMail
      var clear = metadata[elem][0];
      var firstAt = clear.indexOf('@');
      if (firstAt >= 0)
      {
        // does contain '@', get portion up to first '@'
        var hidden = clear.substr(0, firstAt) + '@...';
        // if delimiter is non-'', tack back on everything after it
        var delim = hideEmail[elem];
        if (delim)
          hidden += clear.substr(clear.indexOf(delim, firstAt));
        // emit the munged item and go on to next element
        str += this.keyval2(elem, hidden, true);
        continue;
      }
      // no '@', continue as though elem had not been in hideMail
    }

    // emit unmunged item
    str += this.keyval('', elem, true);
  }

  // done them all, return accumulated string
  return str + '</table>';
},


// transforms a date into a format like January 14, 2000 from numeric parameters
// NOTE: all 3 args MUST be strings
prettyDate:function(year, month, day)
{
  // blug, need to remove lead 0s...
  while ( year.length  &&   year[0]=='0')  year =  year.substr(1);
  while (month.length  &&  month[0]=='0') month = month.substr(1);
  while (  day.length  &&    day[0]=='0')   day =   day.substr(1);
  
  if (!month) month=0;
  if (!day)   day=0;

  /// normalize the parameters
  var n_year  = Math.round(parseInt(year));
  var n_month = Math.round(parseInt(month));
  var n_day   = Math.round(parseInt(day));
  //this.log(n_year+','+n_month+','+n_day);
  
  
  // see which ones are valid
  var year_valid  = (year.length > 0);
  var month_valid = (n_month >= 1  &&  n_month <= 12);
  var day_valid =   (n_day >= 1  &&  n_day <= 31);

  var str = '';
  
  // render the month
  if (month_valid)
  {
    switch (n_month)
    {
    case  1: str += 'January';break;
    case  2: str += 'February';break;
    case  3: str += 'March';break;
    case  4: str += 'April';break;
    case  5: str += 'May';break;
    case  6: str += 'June';break;
    case  7: str += 'July';break;
    case  8: str += 'August';break;
    case  9: str += 'September';break;
    case 10: str += 'October';break;
    case 11: str += 'November';break;
    case 12: str += 'December';break;
    }

    // render the day (only if the month was valid)
    if (day_valid)
      str += ' '+ n_day + (year_valid ? ', ' : '');
    else if (year_valid)
      str += ' ';
  }
  
  // render the year
  if (year_valid)
  {
    str += (n_year >= 0 ? n_year :
            // don't forget really old documents
            (0 - n_year) + ' BCE');
  }
  return str;
},



/* transforms a date into a format like January 14, 2000 from format like one of:
  YYYY-MM-DD hh:mm:ss
  YYYY-MM-DD
  YYYY
*/
toPrettyDate:function(datein)
{
  // bleah, some legacy items have weird nonstandard dates
  // if we think we've found one, just print what it is verbatim.
  // Check if it's only comprised of expected characters.
  var re = /^[0123456789: -]+$/;
  if (!datein.match(re))
    // has some other unexpected char - print AS IS
    return datein;

  
  // normalize paramaters
  // we concat ' -' at end to make 'YYYY' and 'YYYY-MM-DD' inputs work
  var n_date = datein + ' -';
  // extract the fields
  var tmp = n_date.split('-');
  var year =  (tmp.length >=1 ? tmp[0] : '');
  var month = (tmp.length >=2 ? tmp[1] : '');
  var day =   (tmp.length >=3 ? tmp[2] : '');
  // pretty-format the date
  return this.prettyDate(year, month, day);
},


// return a metadata key-value pair
// optional 3rd argument can be passed that evals to true to output as <tr>...
keyval:function(key, val, table)
{
  if (!key)
    key = val;
  
  var val2=this.pr(this.meta.metadata[val]);
  if (!val2)
    return '';

  var str = ((table ? '<tr><td class="key">' : '') +
             '<span class="key">'+ this.ucfirst(key) +':</span> '+
             (table ? '</td><td>' : '') +
             '<span class="value">'+ val2 +'</span>'+
             (table ? '</td></tr>' : '<br/>'));
  return str;
},


// optional 3rd argument can be passed that evals to true to output as <tr>...
keyval2:function(key, val, table)
{
  if (!key)
    key = val;
  if (!val)
    return '';

  var str = ((table ? '<tr><td class="key">' : '') +
             '<span class="key">'+ this.ucfirst(key) +':</span> '+
             (table ? '</td><td>' : '') +
             '<span class="value">'+ val +'</span>'+
             (table ? '</td></tr>' : '<br/>'));
  return str;
},


// given a metadata element, make key-value pair linked to SE query, or
// (if 'multi' is true) a list of all values (semi-colon separated), each linked
// to SE query
// 'queryFunc' optional arg is for formulating custom SE queries (currently used
// only for 'language'); returns array of two values: the text to display, and
// the search query to link it to
metadataWithSearch:function(elem, key, multi, queryFunc) //only 'elem' is required
{
    var metavals = this.meta.metadata[elem];
    if (!metavals)
      return '';
    if (!key)
      key = elem;
    var val = this.makeSearchLink(elem, metavals, (multi ? metavals.length : 1), queryFunc);
    return this.keyval2(key, val);
},


makeSearchLink:function(elem, metavals, n, queryFunc)
{
    var lastsemi = n - 2;
    var val = '';
    for (i = 0; i < n; i++)
      {
        var metaval = metavals[i];
        var queryStr;
        if (queryFunc)
        {
          // may override value of metaval, and provides query string
          // arggh, safari CSJS croaks on this:
          // [metaval, queryStr] = queryFunc(elem, metaval);
          var valAndLink = queryFunc(elem, metaval);
          metaval = valAndLink[0];
          queryStr = valAndLink[1];
        }
        else queryStr = elem+':"'+metaval+'"'; // default query string
        var link = '/search.php?query=' + encodeURIComponent(queryStr);
        val += this.linkify(metaval, link);
        if (i <= lastsemi)
          val += '; ';
      }
    return val;    
},


linkify:function(text, link)
{
    return ((link) ? '<a href="'+link+'">'+text+'</a>' : text);
},


arrayify:function(obj)
{
  return (typeof(obj.length)=='undefined' ? [obj] : obj);
},


// returns keyword link(s)
keywords:function()
{
  // this should go through the key/value template, but the links get clobbered
  if (typeof(this.meta.metadata.subject)=='undefined')
    return '';
  
  // if single element, make array;  if array already, nooop
  var subjects = this.arrayify(this.meta.metadata.subject);

  var links = [];
  for (var i=0, subject; subject=subjects[i]; i++)
  {
    // split input param into parts, separating on ';' char
    var keys = subject.split(';');
    
    for (var k=0, key; key = keys[k]; k++)
    {
      while (key.length  &&   key[0]==' ') //remove lead <SPACE> chars
        key = key.substr(1);

      links.push(this.makeSearchLink('subject',[key],1));
    }
  }

  return (this.css=='education' ?
          links.join('<br/>') :
          this.keyval2('Keywords', links.join('; ')));
},


// used for sorting stream/download 'texts' files
// (FIXXX: okay for now; ultimately, instead of file format names, want to key off of suffixes and
// rely on the knowledge about them that's already embedded in fileLink() )
sortTextsFiles:function(ain,bin)
{
  if (!IAD.seqMap) // create only first time through (this function will be called repeatedly)
    IAD.seqMap = {
        'Single Page Processed JP2 ZIP'  : 0, // bookreader link
        'Single Page Processed TIFF ZIP' : 1, // bookreader link
        'Flippy ZIP'             : 2,
        'Image Container PDF'    : 3,
        'Text PDF'               : 4,
        'Additional Text PDF'    : 5,
        'Standard LuraTech PDF'  : 6, // obsolete, but will still be around for a while
        'Grayscale LuraTech PDF' : 7,
        'EPUB'                   : 8,
        'Daisy'                  : 9,
        'MOBI'                   : 10,
        'HTML'                   : 11,
        'DjVuTXT'                : 12,
        'Original DjVu'          : 13,
        'DjVu'                   : 14,
        'Metadata'               : 15  // _desc.html (ImagePDF books)
    };
  var a = IAD.seqMap[ain.format];
  var b = IAD.seqMap[bin.format];
  return a-b;
},

// used to sort stream/download files for all other mediatypes
sortFiles:function(ain,bin)
{
  var a = ain.size;
  var b = bin.size;
  return a-b;
},

// used to sort review dates
sortReviews:function(ain,bin)
{
  var a = IAD.pr(ain.reviewdate);
  var b = IAD.pr(bin.reviewdate);
  return (a < b ? 1 : (a > b ? -1 : 0));
},

// used to sort <file> elems by <title>
sortByTitle:function(ain,bin)
{
  var a = ain.title;
  var b = bin.title;
  return natcompare(a, b);
},



// store arrays of flvs, h.264s, and mp3s
flowplaysetup:function()
{
  if (this.css=='texts')
    return '';
  
  var mp3v=[], mp36=[], mp3x=[];
  var lenv=[], len6=[], lenx=[];
  var namv=[], nam6=[], namx=[];

  for (var filocation in this.meta.files)
  {
    var fi = this.meta.files[filocation];
    var url= this.identifier + filocation;
    
    var formatLC = this.pr(fi.format).toLowerCase();

    if (this.thumbs.length < 5  &&  (formatLC.indexOf('thumbnail')>=0  ||
                                     filocation.indexOf('.thumbs/') > 0))
    {
      this.thumbs.push(this.meta.dir + filocation);
      continue;
    }

    var suffixLC= url.substr(url.lastIndexOf('.')).toLowerCase();
    var _lastLC = url.substr(url.lastIndexOf('_')).toLowerCase();
      
    var ptr = null;

    /**/ if (_lastLC=='_512kb.mp4' || (formatLC=='512kb mpeg4' ||
                                       formatLC=='h.264 mpeg4'))ptr = this.mp4s;
    else if (suffixLC=='.flv'  ||  suffixLC=='.swf')            ptr = this.flvs;
    else if (formatLC=='ogg video')                             this.ogv = url;
    else if (_lastLC == '_vbr.mp3'  ||  formatLC=='vbr mp3')    ptr = mp3v;
    else if (_lastLC == '_64kb.mp3' ||  formatLC=='64kbps mp3') ptr = mp36;
    else if (suffixLC == '.mp3')                                ptr = mp3x;
    else if (suffixLC == '.srt')                                this.srts.push(url);

    if (ptr)
    {
      // use file's explicit <title> if it exists;
      // else (base (no subdirs)) filename, w/o final suffix
      var name = this.pr(fi.title);
      if (!name)
      {
        name = url.substr(url.lastIndexOf('/')+1);//nix parent dirs..
        name = name.substr(0, name.lastIndexOf('.'));//nix suffix
        if (ptr == this.mp4s)
          name = name.replace(/_512kb$/, '');
      }

      // tracey = lame.  the court submits exhibit #2 on behalf of The People vs..
      // We **LOVE** this item/movie!
      //    http://www.archive.org/details/Sita_Sings_the_Blues
      // but it has a large number of h.264 video <format> files in it
      // and we needed someway to filter out the monstrously large "HD" ones...
      if (formatLC=='h.264 mpeg4'  &&  (name.match(/ HD /) || name.match(/ HD$/)))
        continue; // exclude this video from showing up in the flash/flowplayer

      var len = this.pr(fi.length);

      ptr.push(url);

      /**/ if (ptr == mp3v     ) { namv.push(name); lenv.push(len); }
      else if (ptr == mp36     ) { nam6.push(name); len6.push(len); }
      else if (ptr == mp3x     ) { namx.push(name); lenx.push(len); }
      else if (ptr == this.flvs) { this.flvnames.push(name); }
      else if (ptr == this.mp4s) { this.mp4names.push(name); }
    }
  }

  // special case -- if there are *more* 64kbps MP3s than VBR MP3s, then that
  // can happen when multiple audio tracks of varying formats all derive to
  // the 64kbps derivative.  so we'd rather see "all tracks" even though lo-fi...
  if (mp36.length > mp3v.length  &&  mp3v.length)
    mp3v = [];
  
  /**/ if (mp3v.length)  { this.mp3s=mp3v; this.names=namv; this.lengths=lenv;}
  else if (mp36.length)  { this.mp3s=mp36; this.names=nam6; this.lengths=len6;}
  else if (mp3x.length)  { this.mp3s=mp3x; this.names=namx; this.lengths=lenx;}


  // OK so now we need to figure out *which* flash player to show.
  // For mediatype==movies, we want to always show movies player.
  // For mediatype==audio,  we want to always show audio player.
  // But for anything *else* there could be *either* kind of files.
  // So prioritize movies player first in those cases...
  // (We'll use the array lengths later to determine which player to use...)

  if ((this.flvs.length + this.mp4s.length)  &&  this.css=='movies')
  {
    // movies item w/ 1+ movie!  make it so audio files don't show up and play..
    this.mp3s = [];
  }
  else if (this.mp3s.length  &&  this.css=='audio')
  {
    // audio item w/ 1+ mp3!     make it so movie files don't show up and play..
    this.flvs = [];
    this.mp4s = [];
  }
  else
  {
    if (this.flvs.length + this.mp4s.length)
    {
      // movies!  make it so any audio files don't show up and play..
      this.mp3s = [];
    }
    if (this.mp3s.length)
    {
      // audio!  make it so any movies files don't show up and play..
      this.flvs = [];
      this.mp4s = [];
    }
  }

  if (this.mp3s.length + this.flvs.length + this.mp4s.length == 0)
    return '';

  var showing = (this.mp3s.length ? 'audio' : 'movies');
  //this.log('will show flash for: '+showing);

  
  if (showing == 'movies')
  {
    // found thumbnails (but we'll keep 2nd..5th (because 1st is often all black))
    // if > 4 thumbs gotten above, keep [2nd-5th]; else keep as is
    if (this.thumbs.length==5)
      this.thumbs = this.thumbs.slice(1,5);
  }
  
  
  return (
    '<div id="flowplayercontainer" style="text-align:center;'+
    (showing=='movies' ? '' : 'float:right; width:350px;') + '">' +
    '<div '+(showing=='movies' ? 'style="width:320px; height:240px;"' : '')+
    ' id="flowplayerdiv"> <noscript> <div style="padding:5px; font-size:80%; width:300px; margin-left:auto; margin-right:auto; border:1px dashed gray;">Internet Archive\'s in-browser ' +
    (showing=='movies' ? 'video' : 'MP3') + 
    ' player requires JavaScript to be enabled.  It appears your browser does not have it turned on.  Please see your browser settings for this feature.</div></noscript>' +
    '</div>'+
    '<div id="flowplayerplaylist" style="max-height:200px; overflow:auto;"> '+
    '</div>'+
    // NOTE: IAPlay may not be defined yet as "players.js" may still need to
    // be loaded...
    '<div id="embedthis"><small><a href="http://www.archive.org/about/javascript-required.htm" onclick="if (typeof(IAPlay)==\'undefined\')return false; return IAPlay.embedthis()">embed this</a></small></div>'+
    (location.host.substr(0,4)=='www-' ?
     ' <div id="clipthis"><small><a href="http://www.archive.org/about/javascript-required.htm" onclick="if (typeof(IAPlay)==\'undefined\')return false; return IAPlay.clipthis()">bookmark a clip</a></small></div> ' : '')+
    '</div>');
},



flowplayinsert:function()
{
  if (this.mp3s.length)
  {
    // audio!
    IAPlay.insertPlayer({ 'ignored':'',
          'identifier':this.identifier,
          'mp3s'      :this.mp3s,
          'lengths'   :this.lengths,
          'names'     :this.names });
  }
  else if (this.flvs.length + this.mp4s.length > 0)
  {
    // movies!
    var playlists={ 'ignored':'',
          'mp4s'      :this.mp4s,
          'flvs'      :this.flvs,
          'mp4names'  :this.mp4names,
          'flvnames'  :this.flvnames,
          'thumbs'    :this.thumbs,
          'identifier':this.identifier,
          'server'    :this.meta.server,
          'ogv'       :this.ogv,
          'srts'      :this.srts
    };
    if (this.arg('start'))
    {
      IAPlay.insertPlayer(playlists);
      IAPlay.player.play();
    }
    else
    {
      IAPlay.click2play(playlists);
    }
  }
  return true;
},



// development test function to show thumbnails "inline" on the /details/ page
// and  be able to interact with the flowplayer and jump into movie midstream...
filmstrip:function()
{
  if (!(typeof(location)!='undefined' && location.host=='www-tracey.archive.org'))
    return true; //makes "onclick" action that called us noop and go to real link
  
  var str = '<div onclick="document.getElementById(\'filmstrip\').style.display=\'none\'">';
  var ttl = this.pr(this.meta.metadata.title);
  for (var filocation in this.meta.files)
  {
    var fi = this.meta.files[filocation];
    if (this.pr(fi.format).toLowerCase() == 'thumbnail')
    {
      var sec = filocation.replace(/.*?([1-9][0-9]*)\.jpg/, "$1");
      var ttl2 = sec + ' seconds into \'' + ttl + '\'';
      var target = this.mp4s[0].replace(/\?start=[0-9\.]+/, '', this.mp4s[0]) +
        '?start='+sec;
      str += '<a href="" title="'+ttl2+'" alt="'+ttl2+
        '" onclick="IAD.mp4s[0]=\''+target+'\'; return IAPlay.insertPlayer();"><img style="display:block; margin:10px;" src="http://'+
        this.meta.server + this.meta.dir + filocation + '"/></a>';
    }
  }
  str += '</div>';
  
  var bodyobj = document.getElementsByTagName("body")[0];
  var obj = document.createElement('div');
  obj.setAttribute('id', 'filmstrip');
  obj.setAttribute('style', 'text-align:center; background-color:black; width:180px; position:absolute; top:200px; left:28px;');
  obj.innerHTML = str;
  bodyobj.appendChild(obj);

  return false;
},




// NOTE: format may be null or ''
// sizearg is:
//    "nosize"      to indicate do NOT show size
//    ""            to indicate do NOT show and follow with <br/>
//    a number      to indicate to show size and follow with <br/>
//    "pages"       to indicate to show # pages (if known) and follow with <br/>
fileLink:function(path, format, sizearg, useFormat, isDerived, nlsFilter)
{
  var pathLC = path.toLowerCase();

  var suffixLC      = pathLC.substr(pathLC.lastIndexOf('.'));
  var underIndex    = pathLC.lastIndexOf('_'); // will be -1 if no '_' (/download/<ID>/foo.pdf, etc.)
  var underSuffixLC = pathLC.substr(underIndex);

  // if we're filtering for NLS, skip any unsuitable files
  if (nlsFilter  &&  !this.nlsSuitable(underSuffixLC))
    return '';
  
  // choose the text for the file link

  // default value is <format> for non books
  var name = (useFormat ? format : null);
    
  // these conditions override the value of the format element -->
  /**/ if (underSuffixLC == '_reviews.xml') name = 'XML Reviews';
  else if (underSuffixLC == '_files.xml'  ) name = 'XML List of Files';
  else if (underSuffixLC == '_meta.xml'   ) name = 'XML Metadata';
  else if (underSuffixLC == '_desc.html'  ) name = 'Metadata'; // ImagePDF books
  else if (underSuffixLC == '_flippy.zip')
  {
    // (can get here only if bookreader was previously ruled out)
    name = 'Flip Book';
    path = '/texts/flipbook/flippy.php?id='+this.identifier;
  }
  else if (underSuffixLC == '_jp2.zip' || underSuffixLC == '_tif.zip')
  {
    // (can get here only if we previously confirmed presence of matching scandata)
    name = 'Read Online';
    var prefix = this.getFilenamePrefix(path, underIndex);
    var docPath = ((prefix == this.identifier) ? '' : '/' + prefix);
    path = '/stream/' + this.identifier + docPath;
    sizearg = 'pages';
  }
  else if (underSuffixLC == '_daisy.zip') // path came from "virtual" file object
  {
    name = 'Daisy (beta)';
  }
  else if (underSuffixLC == '_structuralmeta.xml')
  {
    name = 'Contents';
    path = '/texts/articlemeta.php?identifier='+this.identifier;
  }
  else if (format.toLowerCase() == 'hypertext')
  {
    name = 'HTML';
  }
  
  if (!name)
  {
    // these conditions apply if there isn't a format element (or if we're not
    // using it, which is the case for texts items)

    // note that here we select according to suffixLC (and sometimes secondarily
    // by underSuffixLC); files that need to be filtered primarily by underSuffixLC
    // should be dealt with above
    switch (suffixLC)
    {
      // video formats
    case '.mpg'     : name='MPEG1'; break;
    case '.mpeg'    : name='MPEG2'; break;
    case '.mov'     : name='QuickTime'; break;
    case '.avi'     : name='Cinepack'; break;
    case '.wmv'     : name='Windows Media'; break;
    case '.mp4'     : {
      if (path.indexOf('64kb.mp4' )>0) { name='64Kb MPEG4' ; break; }
      if (path.indexOf('256kb.mp4')>0) { name='256Kb MPEG4'; break; }
      if (path.indexOf('512kb.mp4')>0) { name='512Kb MPEG4'; break; }
      name='MPEG4'; break;
    }
      
      // other formats
    case '.gif'     : name='Animated GIF'; break;
    case '.jpg'     : name='Thumbnail'; break;
    case '.jpeg'    : name='Thumbnail'; break;

      // texts formats
    case '.ps'      : name='Postscript'; break;
    case '.doc'     : name='DOC'; break;
    case '.htm'     : name='HTML'; break;
    case '.html'    : name='HTML'; break;
    case '.epub'    : name='EPUB (beta)'; break;
    case '.mobi'    : name='MOBI'; break;
    case '.pdf'     :
      switch(underSuffixLC)
      {
      case '_bw.pdf'   : name='B/W PDF'; break;
      case '_text.pdf' : name='PDF with text'; break;
      default          : name='PDF'; break; // FIXXX: use 'PDF with text' if format is Text PDF
      }
      var source = this.pr(this.meta.metadata.source);
      if (this.offsite(isDerived, source)) // otherwise we take the default
      {
        path = source;
        // extract top two levels of domain (e.g., "google.com") and append to name
        var re = /^http:\/\/[^\/]*?([^.\/]+\.[^.\/]+)\//;
        var mat = re.exec(source);
        if (mat) name += ' (' + this.ucfirst(mat[1]) + ')';
      }
      break;
    case '.txt'     :
      name = 'Full Text';
      path = path.replace(/download/,'stream');
      break;
    case '.djvu'    :
      switch(underSuffixLC)
      {
      case '_orig.djvu'  : name = 'Orig DjVu'; break;
      default            : name = 'DjVu'     ; break;
      }
      path = path.replace(/download/,'stream');
      break;

    default:
      // fall back to using the file name if we can't use the extension
      name = path.substr(path.lastIndexOf('/')+1);
    }
  }

  if (sizearg == 'pages')
    sizearg = this.formatPageCount(this.getFilenamePrefix(path, underIndex));

  var str = (sizearg == 'nosize' ? '' :
             (sizearg == '' ? '' : ' <span>('+
              this.prettySize(sizearg)+')' + '</span>'));

  return (str + '<a href="'+path+'">'+name+'</a>' + // fixxx - use linkify()
          (sizearg == 'nosize' ? '' : '<br/>'));
},

nlsSuitable:function(underSuffixLC)
{
  // start simple, complicate as needed
  return (underSuffixLC.indexOf('_daisy.zip') != -1);
},

getFilenamePrefix:function(path, underIndex)
{
  var skip = ('/download/' + this.identifier + '/').length;
  if (underIndex == -1)
    // no '_', go to last '.' instead
    underIndex = path.lastIndexOf('.');
  return path.slice(skip, underIndex); // between '<id>/' and '_' (or '.'), exclusive
},

formatPageCount:function(prefix)
{
  // until we work out support of multi-document items, including locations for their
  // separate imagecounts, provide page count only for docs whose name is the item identifier
  if ((prefix == this.identifier)  &&  (typeof(this.meta.metadata.imagecount) != 'undefined'))
    return ['verbatim', '~' + this.meta.metadata.imagecount + ' pg'];
  else
    return '';
},

offsite:function(isDerived, source)
{
  var sourceIsUrl = (source.substr(0, 7) == 'http://');
  var re = /^http:\/\/[^\/]*archive\.org\//;
  var urlIsIA = re.test(source);
  return (!isDerived && sourceIsUrl && !urlIsIA);
},


// education helper method
// lists related items and subjects (keywords) for easy browsing
relatedColumn:function()
{
  var str = '<div id="rightside"> ';

  if (typeof(this.meta.metadata.relation)!='undefined')
  {
    var relations = this.arrayify(this.meta.metadata.relation);

    str +=
      '<div id="relations" class="box"><h1>Related Resources</h1>'+
      '<p class="content">';

    for (var i=0, relation; relation=relations[i]; i++)
    {
      var r2 = this.pr(relation);
      if (!r2)
        continue;
      
      str += '<a href="/details/'+r2+'">' +
        (r2.length <= 25 ? r2 : //truncate long titles in the middle
         (r2.substr(0,6) + '..' + r2.substr(r2.length - 14))) + '</a><br/>';
    }
    
    str += '</p></div><br/>';
  }
  

  var subjects = this.keywords();
  if (subjects)
  {
    str +=
      '<div id="subjects" class="box"><h1>Related Subjects</h1>'+
      '<p class="content">'+subjects+'</p></div>';
  }

  return str + '</div>';
},


// education helper method
fileGroup:function(list, banner, sortByTitle)
{
  if (sortByTitle)
    list = list.sort(IAD.sortByTitle);

  var str='';
  for (var i=0, fileptr; fileptr=list[i]; i++)
  {
    var url = '/download/'+ this.identifier + fileptr.location;
    str += (i==0 ? '<h2>'+banner+'</h2>' : '') +
      '<a href="'+url+'">' +fileptr.title+ '</a><br/>';
  }
  return str;
},
  

// for education items -- returns grouped listings of download options
listEducation:function()
{
  var texts=[], videos=[], audios=[], streamings=[], interactives=[], images=[], others=[];
  for (var filocation in this.meta.files)
  {
    var fi  = this.meta.files[filocation];
    var suffixLC = filocation.substr(filocation.lastIndexOf('.')).toLowerCase();
    var formatLC = this.pr(fi.format).toLowerCase();

    var title = this.pr(fi.title);
    if (title)
    {
      var pushitrealgood = false;
      if (suffixLC == '.doc'  ||
          suffixLC == '.txt'  ||
          formatLC.indexOf('text')>=0  ||
          formatLC.indexOf('pdf')>=0  ||
          formatLC.indexOf('html')>=0  ||
          formatLC.indexOf('doc')>=0)
      {
        texts.push(fi);
        pushitrealgood=true;
      }
      if (suffixLC == '.mp4'  ||
          suffixLC == '.mpg'  ||
          suffixLC == '.mpeg'  ||
          suffixLC == '.avi'  ||
          suffixLC == '.mov'  ||
          suffixLC == '.rm'  ||
          formatLC.indexOf('mpeg2')>=0  ||
          formatLC.indexOf('mpeg1')>=0  ||
          formatLC.indexOf('mpeg4')>=0  ||
          formatLC.indexOf('quicktime')>=0  ||
          formatLC.indexOf('real media')>=0)
      {
        videos.push(fi);
        pushitrealgood=true;
      }
      if (suffixLC == '.mp3'  ||
          suffixLC == '.ogg'  ||
          suffixLC == '.wav'  ||
          suffixLC == '.flac'  ||
          suffixLC == '.shn'  ||
          formatLC.indexOf('mp3')>=0)
      {
        audios.push(fi);
        pushitrealgood=true;
      }
      if (suffixLC == '.m3u'  ||
          formatLC.indexOf('m3u')>=0)
      {
        streamings.push(fi);
        pushitrealgood=true;
      }
      if (formatLC.indexOf('flash')>=0)
      {
        interactives.push(fi);
        pushitrealgood=true;
      }
      if (suffixLC == '.jpg'  ||
          formatLC.indexOf('jpeg')>=0)
      {
        images.push(fi);
        pushitrealgood=true;
      }

      if (!pushitrealgood)
      {
        others.push(fi); // file was none of the above categories...
      }
    }
  }

  var str =
    this.fileGroup(texts,       'Readings',                true) +
    this.fileGroup(videos,      'Video Files',             true) +
    this.fileGroup(audios,      'Audio Files',             true) +
    this.fileGroup(streamings,  'Streaming Files',         true) +
    this.fileGroup(interactives,'Interactive Media Files', true) +
    this.fileGroup(images,      'Images',                  true);

  var ret =
    this.fileGroup(others,      'Other Files',             true);

  return str + (ret ? ret : '<h2>Other Files</h2>') + '<br/>';
},


// returns a list of download options
listDownloads:function(mediatype)
{
  var downs = [];
  var perDocMultifileInfo = {}; // for sorting out which documents we have various formats for
  
  for (var filocation in this.meta.files)
  {
    var fi = this.meta.files[filocation];
    var suffixLC = filocation.substr(filocation.lastIndexOf('.')).toLowerCase();
    var formatLC = this.pr(fi.format).toLowerCase();

    if (this.css=='audio'  ||  mediatype=='etree')
    {
      if (formatLC.indexOf('zip')>=0)
        downs.push(fi);
    }
    else if (this.css=='texts')
    {
      var underIndex = filocation.lastIndexOf('_'); // will be -1 if no '_' (scandata.zip, etc.)
      var prefix = ((underIndex == -1)
                    ? this.identifier
                    : filocation.substr(1, underIndex-1)); // skip leading '/'
      var underSuffixLC = filocation.substr(underIndex).toLowerCase();
      if (suffixLC=='.djvu'             ||
          suffixLC=='.pdf'              ||
          suffixLC=='.html'             ||
          suffixLC=='.htm'              ||
          suffixLC=='.doc'              ||
          suffixLC=='.mobi'             ||
          (suffixLC=='.txt' && underSuffixLC!='_meta.txt') || // exclude S3 _meta.txt
          suffixLC=='.ps'               ||
          formatLC=='hypertext'         ||
          underSuffixLC=='_structuralmeta.xml')
      {
        downs.push(fi);
      }
      // save info on which of flippy, image stacks, and scandata we have for this
      // prefix, for later use in deciding whether to use bookreader or flipbook;
      // likewise for abbyy and epub, for use (along with scandata) in deciding whether
      // to offer links to create-on-the-fly epub and daisy
      // (stash in perDocMultifileInfo 2D object)
      else if (underSuffixLC == '_flippy.zip' ||
               underSuffixLC == '_jp2.zip'    ||
               underSuffixLC == '_tif.zip'    ||
               ((suffixLC == '.epub')  &&  (underSuffixLC = '.epub')) || // note that 2nd clause is an assignment
               ((underSuffixLC.substr(0,6)=='_abbyy')  &&  (underSuffixLC = '_abbyy'))) // here, too
      {
        if (typeof(perDocMultifileInfo[prefix]) == 'undefined')
            perDocMultifileInfo[prefix] = {}; // make room if we haven't used [prefix] yet
        perDocMultifileInfo[prefix][underSuffixLC] = fi;
      }
      else if (underSuffixLC == '_scandata.xml' ||
               filocation    == '/scandata.xml' ||
               filocation    == '/scandata.zip')
      {
        if (typeof(perDocMultifileInfo[prefix]) == 'undefined')
            perDocMultifileInfo[prefix] = {};
        perDocMultifileInfo[prefix]['hasScandata'] = 1;
      }
    }
    else if (formatLC.indexOf('mpeg')>=0  ||
             formatLC.indexOf('quicktime')>=0  ||
             formatLC.indexOf('real media')>=0  ||
             formatLC.indexOf('divx')>=0  ||
             suffixLC == '.mp4'  ||
             suffixLC == '.mpg'  ||
             suffixLC == '.mpeg' ||
             suffixLC == '.mov'  ||
             (suffixLC== '.ogg'  &&  formatLC != 'ogg vorbis')  || // Ogg audio
             suffixLC == '.ogv'  ||
             suffixLC == '.avi'  ||
             suffixLC == '.wmv')
    {
      downs.push(fi);
    }
  }

  // for texts items, now go back over what we have for each prefix, and
  // determine (1) whether to use bookreader, flippy, or neither, (2) whether to
  // use epub, generate epub on the fly, or neither, and (3) whether to generate
  // daisy on the fly
  if (this.css == 'texts')
  {
    for (var prefix in perDocMultifileInfo) // should be 'for each...' but not supported in Safari 4.0
    {
      var prefixInfo = perDocMultifileInfo[prefix];
      this.addFilesForBookreader(downs, prefixInfo);
      this.addFilesForEpub(downs, prefix, prefixInfo);
      this.addFilesForDaisy(downs, prefix, prefixInfo);
    }
  }

  if (!downs.length)
  {
    if (this.css=='audio'  ||  mediatype=='etree')
    {
      return '<p class="content"><a href="/compress/'+this.identifier+
        '">Whole directory</a></p>';
    }
    else
    {
      return '';
    }
  }
  
  downs = downs.sort((this.css == 'texts') ? IAD.sortTextsFiles : IAD.sortFiles);
  
  var str = '<p id="dl" class="content">';

  if (typeof(this.meta.misc.nlsitem)!='undefined')
  {
    if (typeof(this.meta.user.nlsuser)=='undefined')
      str += 'This is a restricted item, available only to persons registered with ' +
             'the National Library Service for the Blind and Physically Handicapped. ' +
             'See <a href="/account/printdisabled.php">further info</a>.'; // fixxx - use linkify()
    else
      str += '</p><div style="border: thin black solid; background-color: #AAAAAA; color: #93092D; ' +
             'text-align: center; font-weight: bold;">NLS-authorized user</div><p id="dl" class="content">' +
             this.getFileLinks(downs, true); // 'true' for select only NLS-suitable links
  }
  else
    str += this.getFileLinks(downs, false); // 'false' for don't filter

  str += '</p>';
  return str;
},

addFilesForBookreader:function(downs, prefixInfo)
{
  if (typeof(prefixInfo['hasScandata']) != 'undefined')
  {
    // we have scandata for this prefix; if there's jp2.zip (preferred) or
    // tif.zip, grab it and run
    if (typeof(prefixInfo['_jp2.zip']) != 'undefined')
    {
      downs.push(prefixInfo['_jp2.zip']);
      return;
    }
    else if (typeof(prefixInfo['_tif.zip']) != 'undefined')
    {
      downs.push(prefixInfo['_tif.zip']);
      return;
    }
  }
  // oh well, can't do bookreader as there's no scandata; use flippy if we have it
  if (typeof(prefixInfo['_flippy.zip']) != 'undefined')
    downs.push(prefixInfo['_flippy.zip']);
},

addFilesForEpub:function(downs, prefix, prefixInfo)
{
  // if we have info on an existing .epub, use that; otherwise, see if we can generate
  // one on the fly, and if so, pretend we have one so that we'll offer the link
  if (typeof(prefixInfo['.epub']) != 'undefined')
    downs.push(prefixInfo['.epub']);
  else if (this.canGenFromAbbyy(prefixInfo))
    downs.push(this.virtualFileObject(prefix + '.epub', 'EPUB'));
},

addFilesForDaisy:function(downs, prefix, prefixInfo)
{
  // generate one on the fly if we can
  if (this.canGenFromAbbyy(prefixInfo))
    downs.push(this.virtualFileObject(prefix + '_daisy.zip', 'Daisy'));
},

canGenFromAbbyy:function(prefixInfo)
{
  // note that the first clause below is determined by the item as a whole, not by the
  // files with the given prefix; best we can do for now, until we have fuller support
  // for doc-specific metadata
  return ((this.pr(this.meta.metadata.ocr)   != 'language not currently OCRable')  &&
          (typeof(prefixInfo['_abbyy'])      != 'undefined')  &&
          (typeof(prefixInfo['hasScandata']) != 'undefined'))
},

virtualFileObject:function(loc, format)
{
  // generate a "virtual" file object for formats we'll create on the fly
  return {'location' : '/' + loc,
          'size'     : 'pages',
          'format'   : format};
},

getFileLinks:function(downs, nlsFilter)
{
  var str = '';
  for (var i=0, fi; fi = downs[i]; i++)
  {
    // render a /download/ url to an item file
    // render an item in the downloads list (with file size)
    var format = this.pr(fi.format);
    var url = '/download/' + this.identifier + fi.location;

    var size = this.pr(fi.size);

    var isDerived = (typeof(fi.original) != 'undefined');
    str += this.fileLink(url, format, size, this.css!='texts', isDerived, nlsFilter);
    }
  return str;
},

showOrigAndDerives:function()
{
  var o={};
  var d={};
  
  for (var filocation in this.meta.files)
  {
    var fi = this.meta.files[filocation];
    var fmt= this.pr(fi.format);
    if (fmt.toLowerCase()=='metadata')
      continue;
    
    if (typeof(fi.original)=='undefined')
      o[fmt + '<br/>']=1;
    else
      d[fmt + '<br/>']=1;
  }
  
  var str = '<h2>Original files</h2>'; for (var fmt in o) str += fmt;
  var str2= '<h2>Derived  files</h2>'; for (var fmt in d) str2+= fmt;

  return str + str2;
},

  
// returns files in the streaming list (with connection speed)
listStreams:function(mediatype)
{
  var streams = [];
  var audio = (this.css=='audio'  ||  mediatype=='etree');
  var audioDraw = 0;//boolean
  for (var filocation in this.meta.files)
  {
    var fi = this.meta.files[filocation];
    var suffixLC = filocation.substr(filocation.lastIndexOf('.')).toLowerCase();
    var formatLC = this.pr(fi.format).toLowerCase();

    if (audio)
    {
      // for audio, if no mp3s in item at all, don't show stream section at all!
      // but if mp3s:
      //   if we find files with <format> containing "M3U", then
      //   output the "streaming section" like normal.
      //   else output a "make-streaming-m3u-on-the-fly" link...
      if (formatLC.indexOf('mp3')>=0  ||  suffixLC == '.mp3')
        audioDraw = 1;
      if (formatLC.indexOf('m3u')>=0)
        streams.push(fi);
    }
    else
    {
      if (formatLC.indexOf('m3u')>=0  ||
          formatLC.indexOf('real media')>=0  ||
          suffixLC == '.rm')
      {
        streams.push(fi);
      }
    }
  }
  

  var str =
    '<h2>Stream (<a href="/about/faqs.php#94">help' +
    this.imgHelper('/images/question.gif', 'help1', 'help',0,'style="vertical-align:middle"')+
    '</a>)</h2><p class="content">';

  if (!streams.length)
  {
    if (audioDraw)
    {
      // no M3U found, but MP3s were.  point to a "m3u-on-the-fly" link
      return str + '<a href="/stream/'+this.identifier+
        '">MP3 via M3U</a></p>';
    }
    
    return '';
  }

  
  streams = streams.sort(IAD.sortFiles);
  //this.log(streams);
  
  for (var i=0, fi; fi = streams[i]; i++)
  {
    // render a /stream/ url to an item file
    var format = this.pr(fi.format);
    var url = 'http://www.archive.org/'+(audio ? 'download':'stream')+'/'+
      this.identifier + fi.location;
    str += this.fileLink(url, format, 'nosize', this.css!='texts');

    var kb = parseInt(format.substring(0, format.indexOf('Kb')));
    if (audio)
    {
      str += ' (' + (kb == 64 ? 'Lo-Fi' : 'Hi-Fi') + ')';
    }
    else
    {
      if (kb  &&  kb <= 200)
      {
        str += ' (dialup)';
      }
      else if (kb  &&  kb > 200)
      {
        str += ' (broadband)';
      }
      else 
      {
        var mb = parseInt(format.substring(0, format.indexOf('Mb')));
        if (mb  &&  mb > 0)
          str += ' (broadband)';
      }
    }
    
    str += '<br/>';
  }

  str += '</p>';
  return str;
},


//############################################################################
//  RENDERS JSON ==> HTML
//############################################################################
render:function()
{
  var SSJS = (typeof(document)=='undefined');

  if (this.meta)
    return false;
   
  if (SSJS)
    this.deferImages = null;  // SSJS -- don't use JS to later add <img>s

  this.meta = meta;
  this.css  = meta.misc.css;
  // NOTE: takes the 1st collection if array; else as is if single elem
  var collection = this.pr(meta.metadata.collection);
  var identifier = meta.dir.substr(meta.dir.lastIndexOf('/')+1);
  var mediatype  = this.pr(meta.metadata.mediatype);
  var stream_only= this.pr(meta.metadata.stream_only);
  this.identifier = identifier;
  


  // NOTE: education has to output right column FIRST since it floats ;-)
  var str = (this.css=='education' ? this.relatedColumn() : '');
  
  
  //############################################################################
  //  START OF LEFT/NARROW COLUMN
  //############################################################################
  str += '<div id="col1"> <div class="box">'+
    (this.css=='education' ? '' :
     '<h1>'+this.ucverb[this.css]+this.mediatypeItem[this.css]+'</h1>');

  var tmp = this.pr(meta.metadata.runtime);
  str +=
    '<div style="text-align:center;">' +
    this.thumbnail() + '<br/>' +
    (this.css != 'movies' ? '' :
     '<a onclick="return IAD.filmstrip();" href="/movies/thumbnails.php?identifier='+identifier+
     '">View thumbnails</a><br/>') +
    (tmp ? 'Run time: ' + tmp : '') +
    '</div>';

  if (this.css=='education')
  {
    str += this.listEducation();
  }
  else
  {
    if (this.css!='texts'  &&
        (typeof(location)!='undefined' && location.host=='www-tracey.archive.org'))
    {
      str += this.showOrigAndDerives();
    }

    if (this.css!='texts')
      str += this.listStreams(mediatype);
    
    if (!stream_only)
    {
      if (this.css!='texts')
      {
        str +=
          '<h2>Play / Download (<a href="/about/faqs.php#'+
          (this.css=='audio' || mediatype=='etree' ? 'Audio' : 'Movies')+
          '">help' + this.imgHelper('/images/question.gif', 'help2',
                                    'help',0,'style="vertical-align:middle"') +
          '</a>)</h2>';

      }
      str += this.listDownloads(mediatype) + '<br/>';
      
    }
  }

  if (typeof(this.meta.misc.nlsitem)=='undefined')
  {
    if (!stream_only)
    {
      str +=
        '<b>All Files: </b><a href="http://'+meta.server + meta.dir+ // fixxx - use linkify() here and below
        '">HTTP</a><br/>';
    }

    if (this.css=='texts')
    {
      str += '<br/><div style="text-align:center;"><a href="http://www.archive.org/about/faqs.php#Texts_and_Books">Help reading texts</a></div>';
    }
  }
  

  var ret = this.ccimage();

  str +=
    (ret  &&  this.css=='education' ? '<h2>Licensing Information</h2>' : '') +ret+
    '</div> ';


  var admins = this.pr(meta.user.admins);
  str += 
    ((this.css!='texts'  ||  !admins  ||  (typeof(this.meta.misc.nlsitem)!='undefined')) ? '' :
     '<div class="box"><h1>Print on demand</h1><p class="content">' + this.textsPOD() + '</p></div>') +
    (!admins ? '' :
     '<div class="box"><h1>Admins</h1><p class="content">'+admins+'</p></div>')+
  
    '<div class="box"><h1>Resources</h1>';
  
  
  var deadlists = '';
  if (collection=='GratefulDead')
  {
    // input date MUST start with format: "YYYY-MM-DD"
    var ddate = this.pr(meta.metadata.date);
    if (ddate)
    {
      var mon = ddate.substr(5,2);
      var day = ddate.substr(8,2);
      // need to remove lead 0s
      while (mon.length && mon[0]=='0') mon = mon.substr(1);
      while (day.length && day[0]=='0') day = day.substr(1);
      var deaddate = mon + '/' + day + '/' + ddate.substr(2,2);
      var url = 'http://deadlists.com/deadlists/showresults.asp?KEY='+deaddate;

      deadlists = '<a href="'+url+'" onclick="javascript:window.open(\''+url+
        '\',\'popup\',\'width=800,height=600,scrollbars=yes,resizable=yes,toolbar=no,directories=no,location=no,menubar=no,status=no\'); return false;">DeadLists Project</a><br/>';
    }
  }

  str +=
    deadlists +

    (collection!='ourmedia' ? '' :
     '<a href="http://www.ourmedia.org/ia/details/'+identifier+
     '">Item at Ourmedia.org</a><br/>') +
    
    '<a href="/bookmarks.php?add_bookmark=1&amp;mediatype='+mediatype+
    '&amp;item_identifier='+identifier+'&amp;title='+encodeURIComponent(this.pr(meta.metadata.title))+
    '">Bookmark</a><br/>'+
                                                         
    (this.pr(meta.user.editable) > 0 ?
     '<a href="/edit/'+identifier+'">Edit item</a><br/>' : '') +
    
    '</div>' +
    

    '</div>';



  

  //############################################################################
  //  START OF CENTER/MAIN COLUMN
  //############################################################################
  // title bar
  // min-width of 650px because h.264 clips can insert player 640px wide
  str += '<div style="margin-left:210px; ' +
    (this.css=='education' ? 'margin-right:215px;' : 'min-width:650px;')+
    '"><div class="box"><h1 style="font-size:125%;">'+
    '<span style="float:right;">' +
    (this.css=='movies'  &&  this.pr(meta.metadata.director) ?
     this.pr(meta.metadata.director) :
     (this.css=='texts' ? '' : this.pr(meta.metadata.creator))) +
    '</span>' +
    this.pr(meta.metadata.title) +
    
    (this.pr(meta.metadata.date) ? ' ('+this.toPrettyDate(this.pr(meta.metadata.date))+')':
     '') +
  '</h1>';


  if (stream_only)
  {
    //alternate text option for certain streaming shows
    var tmp = this.pr(meta.metadata.post_text);
    if (!tmp)
    {
      tmp = 'This '+this.mediatypeItem[this.css] +
        ' is available in streaming format';
    }
    str += '<p class="statusMessage">'+tmp+'</p>';
  }

  str += this.flowplaysetup();

  str +=
    '<p class="content">' +  // START OF VERY BIG "PARAGRAPH"
    ((mediatype=='etree' || this.css=='texts') ? '' :
     this.pr(meta.metadata.description).replace(/\n/g, '<br/>'))  +
    '<br/>';
  

  if (mediatype!='etree'  &&  this.css!='texts')
  {
    str += (this.pr(meta.misc['collection-title']) ?
            ('<br/><br/>' + this.keyval2(
              'This '+this.mediatypeItem[this.css]+' is part of the collection',
              '<a href="/details/'+
              collection+'">'+this.pr(meta.misc['collection-title'])+'</a><br/>'))
            : '');
  }



  
  var av = this.pr(meta.metadata.sound);
  if (av  &&  this.pr(meta.metadata.color))
    av += ', ';
  av += this.pr(meta.metadata.color);

  
    
  if (mediatype=='etree')
  {
    var date    = this.pr(meta.metadata.date);
    str +=
      (collection ?
       this.keyval2('Collection: ',
                    '<a href="/details/'+collection+'">'+collection+'</a>'):'') +
      this.metadataWithSearch('creator', 'Band/Artist') +
      (date ?
       this.keyval2('Date',
                    this.toPrettyDate(date) +
                    '<span style="font-size:8pt;"> '+
                    '(<a href="/search.php?query=' +
                    encodeURIComponent('creator:"'+this.pr(meta.metadata.creator)+
                                       '" AND date:'+date+'*')+
                    '">check for other copies</a>)' +
                    '</span>') : '' ) +
                    
      this.metadataWithSearch('venue') +
      this.metadataWithSearch('coverage', 'Location') +
      '<br/>'+
      this.keyval('', 'source')+
      this.keyval('', 'lineage')+
      this.metadataWithSearch('taper', 'Taped by')+
      this.metadataWithSearch('transferer', 'Transferred by')+
      '';
  }
  else if (this.css=='movies')
  {
    str += 
      this.keyval('', 'director') +
      this.keyval('Producer', 'creator') +
      this.keyval('Production Company', 'publisher') +
      this.keyval('', 'sponsor') +
      this.keyval2('Audio/Visual', av) +
      '';
  }
  else if (this.css=='audio')
  {
    str += this.metadataWithSearch('creator', 'Artist/Composer');
    str +=
      this.keyval('', 'date') +
      this.keyval('', 'source') +
      this.keyval('Label / Recorded by', 'publisher') +
      this.keyval('Product Code', 'catalog_number') +
      this.metadataWithSearch('contributor', 'Contributor') +
      '';
  }
  else if (this.css=='home')
  {
    // show the remaining metadata fields
    var ignoreFields = {'collection':1,
                        'description':1,
                        'notes':1,
                        'md5s':1,
                        'md5contents':1,
                        'updater':1,
                        'updatedate':1,
                        'uploader':1,
                        'adder':1,
                        'subject':1,// (done later on)
                        'title':1,
                        'contact':1,
                        'language':1};
    
    var linkFields = {'creator':1,
                      'mediatype':1,
                      'publisher':1};
    for (var fieldName in meta.metadata)
    {
      if (typeof(ignoreFields[fieldName])!='undefined')
        continue;

      var values = this.arrayify(meta.metadata[fieldName]);
      for (var i=0; i < values.length; i++)
      {
        var value = this.pr(values[i]);
        if (value=='')
          continue;
        
        if (typeof(linkFields[fieldName])!='undefined')
          str += this.keyval2(fieldName,this.makeSearchLink(fieldName,[value],1));
        else
          str += this.keyval2(fieldName, value);
      }
    }
  }
  else if (this.css=='education')
  {
    var date = this.pr(meta.metadata.date);

    // (close and reopen the surrounding <p>, because <p> can't contain <h2> or <p>)
    str += ('</p><h2>About this Item</h2>' +
            '<table>' +
            this.keyval('', 'director', 1) +
            this.keyval('', 'audience', 1) +
            this.keyval('', 'contributor', 1) +
            this.keyval('', 'creator', 1) +
            (date ? this.keyval2('Date', this.toPrettyDate(date), 1) : '') +
            this.keyval('', 'language', 1) +
            this.keyval2('Audio/Visual', av, 1));

    if (typeof(meta.metadata.user_category)!='undefined')
    {
      var tmp = this.arrayify(meta.metadata.user_category);

      str += '<tr><td style="vertical-align: top;"><span class="key">Audience:</span></td><td>';
      for (var i=0; i < tmp.length; i++)
      {
        var uc = this.pr(tmp[i]);
        if (!uc)
          continue;

        str += uc + '<br/>';
      }

      str += '</td></tr>';
    }
    
    str +=
      this.keyval('Contact Information', 'contact') +
      
      '</table><p class="content">';
  }
  else if (this.css=='texts')
  {
    str +=

        // -- author(s) --
        this.metadataWithSearch('creator', 'Author', true) +

        // -- volume --
        this.keyval('', 'volume') +

        // -- subject(s) --
      (collection == 'opensource' ? this.keywords() :
       this.metadataWithSearch('subject', null, true)) +

        // -- publisher --
        this.metadataWithSearch('publisher') +

        // -- pub year --
        this.metadataWithSearch('year') +

        // -- copyright status --
        this.keyval('Possible copyright status', 'possible-copyright-status') +

        // -- language --
        this.metadataWithSearch('language', null, true, this.textsLanguage) +

        // -- call number --
        this.keyval('Call number', 'call_number') +

        // -- sponsor -- (also does usage rights, if specified for the sponsor)
        this.textsSponsor(this.pr(meta.metadata.sponsor)) +

        // -- contributor --
        this.textsContributor(this.pr(meta.metadata.contributor), this.pr(meta.metadata.sponsor)) +

        // -- collection(s) --
        this.metadataWithSearch('collection', null, true) +

        // -- notes --
        this.keyval('', 'notes') +

        // -- scanfactors --
        this.keyval('', 'scanfactors') +

        // -- Open Library/booki link --
        (typeof(this.meta.misc.ol_link) == 'undefined' ? '' :
         '<br/>' +
         this.linkify(this.imgHelper('/images/open-library-icon.gif', 'ol_icon',
                                     'Open Library icon', '16px', 'style="vertical-align:middle"'),
                      'http://openlibrary.org') +
         ' This book has an ' + this.linkify('editable web page', this.meta.misc.ol_link) + ' on ' +
         this.linkify('Open Library', 'http://openlibrary.org/') + '.' +
	 // temp: for this book only, add hardcoded booki link
         ((identifier != 'adventuresoftoms00twaiiala') ? ''
          : ' You can ' + this.linkify('correct errors', 'http://booki.flossmanuals.net/the-adventures-of-tom-sawyer/the-adventures-of-tom-sawyer/edit/') +
            ' in the text version.')) +

        // fixme PDFs -- later could have each tog() *also* flip + to - ...
        (typeof(this.meta.misc.pages)=='undefined' ? '' :
       '<div style="padding-left:10px; font-size:16pt; font-weight:bold;">View PDFs</div> <div id="pdfs" style="font-weight:bold; font-size:10pt;"> <span style="font-size:8pt; font-weight:normal; font-style:italic;">the microfilm PDF viewer requires javascript</span> </div>') +

        // The EAD viewer is modelled on the PDFs viewer.
        (typeof(this.meta.misc.ead_container)=='undefined' ? '' :
       '<div style="padding-left:10px; font-size:16pt; font-weight:bold;">View EAD</div><iframe src="http://'+this.meta.server+'/ead/EADLoad.php?id='+this.identifier+'&ead='+this.meta.dir+'/'+this.identifier+'_ead.xml&viewOnly=1" width="100%" height="450px"></iframe>') +

        // -- description -- (close and reopen the surrounding <p>, because <p> can't contain <h2> or <p>)
        '</p>' + 
        (meta.metadata.description
         // is present, display
         ? this.textSection('description', true)
         // not present; if user has edit privs, offer to let them make one
         : ((this.pr(meta.user.editable) > 0)
            ? '<h2>Description</h2><p class="content"><a href="/editxml.php?edit_item=' +
            identifier + '&amp;type=item">Add</a> a description.</p>'
            : '')) + '<p class="content">' +
  
        // LOANABLE BOOKS (likewise here on closing/reopening <p> because we have <h2>)
        ((typeof(this.meta.user.acs4_link) != 'undefined'  &&  admins) // FIXXX - del "admins" after demo
         ? '</p><h2>Internet Archive Lending Library ' +
           '<span style="color: #C33C36; background-color: white; padding: 1px">Beta Test!</span></h2>' +
           '<p>' +
           'You may borrow this book using <a href="http://www.adobe.com/products/digitaleditions/">Adobe ' +
           'Digital Editions</a> software. You may also transfer this book to certain eBook readers, ' +
           'including the <a href="http://ebookstore.sony.com/reader/">Sony Reader</a>.</p>' +
           '<p class="content">' +
           '<b><a href="'+this.meta.user.acs4_link+'">Borrow this book</a></b>'
         : '') +

        '';
  }

  
  
  if (this.css=='movies'  ||  this.css=='home'  ||
      this.css=='audio'  ||  mediatype=='etree')
  {
    str +=
      this.keyval('', 'language') +
      
      // Keywords
      this.keywords() +

      this.keyval('Contact Information', 'contact') +
      '';
  }
  
  
  str += '</p>'; // END OF VERY BIG "PARAGRAPH"

  str += this.cclicense();


  str += '<br style="clear:right;"/>'; // Clear float right from any flash player!


  if (mediatype=='etree')
    str += this.textSection('description');
  else if (this.css=='audio')
    str += this.textSection('notes');

  if (this.css=='movies'  ||  this.css=='home'  ||
      this.css=='audio'  ||  mediatype=='etree')
  {
    str += this.downloadTable(stream_only);
  }
  

  // REVIEWS SECTION (INCLUDES "NUMBER TIMES DOWNLOADED" IN BANNER TOO)
  var reviewsL = [];
  if (typeof(meta.reviews)!='undefined'  &&
      typeof(meta.reviews.reviews)!='undefined')
  {
    reviewsL = meta.reviews.reviews;
  }
  

  var dl = (typeof(meta.item)!='undefined' ? this.pr(meta.item.downloads) : '');
  if (dl)
    dl = this.formatCommas(' '+dl);

  str += '<h2 style="font-size:125%;"><span class="rightmost" style="font-weight:bold;"><a href="/write-review.php?identifier='+
    identifier+ '">'+(reviewsL.length?'Write':'Be the first to write')+
    ' a review</a>';
  if (dl)
    str += '<br/><span style="font-size:80%">' +
           ((typeof(this.meta.user.acs4_link) != 'undefined')
            ? 'Borrowed '
            : (stream_only ? 'Streamed ' : 'Downloaded ')) +
           dl + ' times</span>';

  str += '</span>Reviews<br/><span style="font-weight:bold; font-size:80%">'+
  (reviewsL.length ?
   'Average Rating: ' + this.stars(meta.reviews.info.avg_rating) :
   // So... no reviews.  if dl > 0, add blank line so colored bg extends to
   // (floated) dl count
   (dl ? '<br/>' : ' ')) +
  '</span></h2>';

  
  str += this.reviews(reviewsL);
  
  if (mediatype=='etree'  ||  this.css=='home')
  {
    str += this.textSection('notes');
  }
  else if (this.css=='movies')
  {
    str +=
      this.textSection('credits') +
      this.textSection('segments') +
      this.textSection('shotlist');
  }
  else if (this.css=='education')
  {
    str +=
      this.textSection('credits')+
      this.textSection('segments')+
      this.textSection('shotlist');
  }
  else if (this.css=='texts')
  {
    str += this.textsSelMetadata(this.meta.metadata);
  }


  
  str += '</div></div>';

  // end of page!
  str += '<div><hr style="clear:both;"/></div><p id="iafoot"><a href="/about/terms.php">Terms of Use (10 Mar 2001)</a></p>';


  
  if (SSJS)
  {
    // SSJS!

    if (this.SSJS_ONLY)//fixme oh the hackery of this kind of copying...
    {
      var copies=['css','identifier',
                  'names','lengths','mp3s',
                  'flvs','mp4s','thumbs','flvnames','mp4names','ogv','srts'];
      str += "\n<script type=\"text/javascript\">\n";

      for (var i=0, cp; cp = copies[i]; i++)
        str += 'IAD.'+cp+' = '+JSON.stringify(this[cp])+";\n";

      str += "IAD.meta = {\"server\":\""+this.meta.server+"\""+
        ',"misc":{'+
        (this.meta.misc.pages ? '"pages":'+
         JSON.stringify(this.meta.misc.pages) : '') +
        (this.meta.misc.ead_container ? '"ead_container":'+
         JSON.stringify(this.meta.misc.ead_container) : '') +
        "}}\n";
      str += "IAD.render2();\n";
      str += "</script>\n";
    }

    print(str);

    if (!this.SSJS_ONLY)
    {
      print('<script type="text/javascript">document.getElementById("flowplayerdiv").innerHTML=\'<div style="padding:5px; font-size:80%; width:300px; margin-left:auto; margin-right:auto; border:1px dashed gray;">To see the in-browser media player please <a href="/details/'+
            this.identifier+'&amp;output=">click here<\\/a>.<div>\';</script>');
    }
    return false;
  }


  

  // make a new element with all our "rendered" HTML from above and place it
  // at the end of <body>
  var bodyobj = document.getElementsByTagName("body")[0];
  var obj = document.createElement('span');
  obj.setAttribute('id', 'tab');
  obj.innerHTML = str;
  bodyobj.appendChild(obj);
  

  // update any <img> "src" attributes that we wanted deferred now
  // (so they will only start loading now and avoid hanging up any CSJS)
  if (this.deferImages)
  {
    for (var imgid in this.deferImages)
    {
      var obj = document.getElementById(imgid);
      if (obj)
      {
        obj.style.height = null;
        obj.src = this.deferImages[imgid];
        //this.log('loading '+obj.src+' into '+imgid);
      }
    }
  }


  this.render2();
  return false;
},


// NOTE!  while this.SSJS_ONLY is true, we had to manually "copy" everything
// referencing "this" in function below because we are split between SSJS and CSJS
render2:function()
{
  // IE is lame when it comes to tables being 100% wide...
  if (navigator.userAgent.indexOf('IE')>=0)
  {
    for (var i=0; i<10; i++)
    {
      // pick a "known safe" max width (search for "min-width" further above)
      var obj = document.getElementById('ff'+i);
      if (obj)
        obj.style.width = '640px'; 
    }
  }
  
  
  // if there are no media files appropriate for our flash player, we won't
  // bother with wasting time loading flash javascript,etc. below
  var num2flash = this.flvs.length + this.mp4s.length + this.mp3s.length;
  //this.log('(render2) num2flash:' + num2flash);

  // OK now that the page has started rendering in the client to HTML by
  // the above addition to <body>, get the audio/video player script stuff
  // loaded.  we trigger the flash player to setup and embed in the
  // page body's "onload" event...
  if (num2flash  &&  this.css!='texts')
  {
    this.loadme = 'flash';
    return;
  }
  
  if (this.css=='texts')
  {
    if (typeof(this.meta.misc.pages)!='undefined')
    {
      var headobj = document.getElementsByTagName("head")[0];         
      var obj = document.createElement('script');
      obj.setAttribute('type','text/javascript');
      obj.setAttribute('src', '/includes/pdfs.js');
      headobj.appendChild(obj);

      this.loadme = 'pdfs';
    }
    else if (typeof(this.meta.misc.ead_container)!='undefined')
    {
      this.loadme = 'ead_container';
    }
  }
},


// NOTE!  while this.SSJS_ONLY is true, we had to manually "copy" everything
// referencing "this" in function below because we are split between SSJS and CSJS
bodyLoaded:function()
{
  this.log('bodyLoaded');
  if (this.loadme=='flash')
  {
    this.flowplayinsert();
  }
  else if (this.loadme=='pdfs')
  {
    drawy(this.identifier, this.meta.misc.pages);
  }
  else if (this.loadme=='ead_container')
  {
    EAD.draw_ead_container(this.meta.misc.ead_container);
  }

  //if (navigator.userAgent.indexOf('Firefox'))
  //  console.timeEnd('page');
}

}; // end IAD definition




var EAD = { // This is to encapsulate how we draw the EAD viewer
  draw_ead_container:function(ead_container)
  {
    return; //use IAEADEdit instead
    var o=document.getElementById('ead_container');
    var str='';
    var no_data=true;
    if (typeof(ead_container.title)!='undefined')
    {
      str += '<div class="ead_title">';
      str += ead_container.title;
      str += '</div>';
      no_data=false;
    }
    if (typeof(ead_container.containers)!='undefined')
    {
      var count=ead_container.containers.length;
      if (count>0) {
        no_data=false;
        for (var i=0;i<count;i++)
        {
          var i1=i+1;
          container=ead_container.containers[i];
          var ctitle='container ' + i1;
          str += '<a href="#'+ctitle+
            '" onclick="return EAD.tog(\'co_'+i+
            '\')"+\')">+<img src="/images/folder.png"/> <u>Series '+i1+
            '</u></a>';
          str += '<div class="c01" id="co_'+i+'">';
          if (typeof(container.subcontainers)!='undefined')
          {
            var subcount=container.subcontainers.length;
            for (var j=0;j<subcount;j++) {
              var j1=j+1;
              subcontainer=container.subcontainers[j];
              var sctitle='subcontainer ' + j1;
              str += '<a class="c02" href="#'+sctitle+
                '" onclick="return EAD.tog(\'subco_'+j+
                '\')"+\')">+<img src="/images/folder.png"/><u>  File '+j1+
                '</u></a>';
              str += '<div class="c02" id="subco_'+j+'">';
              if (typeof(subcontainer.link)=='undefined') {
                str += "missing link\n";
              } else {
                str += '<a class=c02_link href="' + subcontainer.link +
                  '">';
                if (typeof(subcontainer.unittitle)=='undefined') {
                  str += 'link';
                } else {
                  str += subcontainer.unittitle;
                }
                str += '</a>';
              }
              str += '</div><br>'; // close up subcontainer j
            }
          }
          str += '</div>'; // close up container i
        }
      }
    }
    if (no_data) str += '<div><pre>No EAD data to view</pre></div>';
    o.innerHTML=str;
  },
  // makes a hidden div become visible; makes a visible div become hidden
  // (from pdfs.js)
  tog:function(id)
  {
    var o=document.getElementById(id);
    if (o.style.display=='block')
      o.style.display='none';
    else
      o.style.display='block';
    return false;
  }
}; // end of EAD definition


// natural comparison
// minified FROM http://sourcefrog.net/projects/natsort/natcompare.js
function isWhitespaceChar(B){var A;A=B.charCodeAt(0);if(A<=32){return true}else{return false}}function isDigitChar(B){var A;A=B.charCodeAt(0);if(A>=48&&A<=57){return true}else{return false}}function compareRight(E,B){var G=0;var F=0;var D=0;var C;var A;for(;;F++,D++){C=E.charAt(F);A=B.charAt(D);if(!isDigitChar(C)&&!isDigitChar(A)){return G}else{if(!isDigitChar(C)){return -1}else{if(!isDigitChar(A)){return +1}else{if(C<A){if(G==0){G=-1}}else{if(C>A){if(G==0){G=+1}}else{if(C==0&&A==0){return G}}}}}}}}function natcompare(I,H){var C=0,A=0;var D=0,B=0;var F,E;var G;while(true){D=B=0;F=I.charAt(C);E=H.charAt(A);while(isWhitespaceChar(F)||F=="0"){if(F=="0"){D++}else{D=0}F=I.charAt(++C)}while(isWhitespaceChar(E)||E=="0"){if(E=="0"){B++}else{B=0}E=H.charAt(++A)}if(isDigitChar(F)&&isDigitChar(E)){if((G=compareRight(I.substring(C),H.substring(A)))!=0){return G}}if(F==0&&E==0){return D-B}if(F<E){return -1}else{if(F>E){return +1}}++C;++A}};

//minified FROM http://www.json.org/json2.js (only needed when SSJS_ONLY==true!)
if(!this.JSON){JSON=function(){function f(n){return n<10?"0"+n:n}Date.prototype.toJSON=function(key){return this.getUTCFullYear()+"-"+f(this.getUTCMonth()+1)+"-"+f(this.getUTCDate())+"T"+f(this.getUTCHours())+":"+f(this.getUTCMinutes())+":"+f(this.getUTCSeconds())+"Z"};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,escapeable=/[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,gap,indent,meta={"\b":"\\b","\t":"\\t","\n":"\\n","\f":"\\f","\r":"\\r",'"':'\\"',"\\":"\\\\"},rep;function quote(string){escapeable.lastIndex=0;return escapeable.test(string)?'"'+string.replace(escapeable,function(a){var c=meta[a];if(typeof c==="string"){return c}return"\\u"+("0000"+(+(a.charCodeAt(0))).toString(16)).slice(-4)})+'"':'"'+string+'"'}function str(key,holder){var i,k,v,length,mind=gap,partial,value=holder[key];if(value&&typeof value==="object"&&typeof value.toJSON==="function"){value=value.toJSON(key)}if(typeof rep==="function"){value=rep.call(holder,key,value)}switch(typeof value){case"string":return quote(value);case"number":return isFinite(value)?String(value):"null";case"boolean":case"null":return String(value);case"object":if(!value){return"null"}gap+=indent;partial=[];if(typeof value.length==="number"&&!(value.propertyIsEnumerable("length"))){length=value.length;for(i=0;i<length;i+=1){partial[i]=str(i,value)||"null"}v=partial.length===0?"[]":gap?"[\n"+gap+partial.join(",\n"+gap)+"\n"+mind+"]":"["+partial.join(",")+"]";gap=mind;return v}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{for(k in value){if(Object.hasOwnProperty.call(value,k)){v=str(k,value);if(v){partial.push(quote(k)+(gap?": ":":")+v)}}}}v=partial.length===0?"{}":gap?"{\n"+gap+partial.join(",\n"+gap)+"\n"+mind+"}":"{"+partial.join(",")+"}";gap=mind;return v}}return{stringify:function(value,replacer,space){var i;gap="";indent="";if(typeof space==="number"){for(i=0;i<space;i+=1){indent+=" "}}else{if(typeof space==="string"){indent=space}}rep=replacer;if(replacer&&typeof replacer!=="function"&&(typeof replacer!=="object"||typeof replacer.length!=="number")){throw new Error("JSON.stringify")}return str("",{"":value})},parse:function(text,reviver){var j;function walk(holder,key){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)}cx.lastIndex=0;if(cx.test(text)){text=text.replace(cx,function(a){return"\\u"+("0000"+(+(a.charCodeAt(0))).toString(16)).slice(-4)})}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,""))){j=eval("("+text+")");return typeof reviver==="function"?walk({"":j},""):j}throw new SyntaxError("JSON.parse")}}}()};
