text: /*
   * Copyright 2009, Google Inc.
   * All rights reserved.
   *
   * Redistribution and use in source and binary forms, with or without
   * modification, are permitted provided that the following conditions are
   * met:
   *
   *     * Redistributions of source code must retain the above copyright
   * notice, this list of conditions and the following disclaimer.
   *     * Redistributions in binary form must reproduce the above
   * copyright notice, this list of conditions and the following disclaimer
   * in the documentation and/or other materials provided with the
   * distribution.
   *     * Neither the name of Google Inc. nor the names of its
   * contributors may be used to endorse or promote products derived from
   * this software without specific prior written permission.
   *
   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
   * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
   * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
   * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
   * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
   * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
   * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
   * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
   * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
   * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
   */
  
  
  /**
   * @fileoverview The jsdoctoolkit loads this file and calls publish()
   */
  
  
  var g_symbolSet;  // so we can look stuff up below.
  var g_filePrefix;
  var g_validJSDOCTypes = {
    'number': true,
    'Number': true,
    'object': true,
    'Object': true,
    '*': true,
    '...': true,
    'string': true,
    'String': true,
    'void': true,
    'undefined': true};
  var g_unknownTypes = { };
  var g_numErrors = 0;
  
  /**
   * Called automatically by JsDoc Toolkit.
   * @param {SymbolSet} symbolSet Set of all symbols in all files.
   */
  function publish(symbolSet) {
    publish.conf = {  // trailing slash expected for dirs
      ext: '.ezt',
      outDir: JSDOC.opt.d || SYS.pwd + '../out/jsdoc/',
      templatesDir: JSDOC.opt.t || SYS.pwd + '../templates/jsdoc/',
      symbolsDir: '',
      prefix: JSDOC.opt.D.prefix || 'js_1_0_'};
    publish.conf.srcDir = publish.conf.outDir + 'src/'
    publish.conf.htmlDir = publish.conf.outDir + 'original_html/'
  
    // is source output is suppressed, just display the links to the source file
    if (JSDOC.opt.s && defined(Link) && Link.prototype._makeSrcLink) {
      Link.prototype._makeSrcLink = function(srcFilePath) {
        return '&lt;' + srcFilePath + '&gt;';
      }
    }
  
    // create the folders and subfolders to hold the output
    IO.mkPath((publish.conf.htmlDir).split('/'));
  
    // used to allow Link to check the details of things being linked to
    Link.symbolSet = symbolSet;
    Link.base = '../';
  
    // used to allow other parts of this module to access database of symbols
    // and the file prefix.
    g_symbolSet = symbolSet;
    g_filePrefix = publish.conf.prefix;
  
    // create the required templates
    try {
      var templatesDir = publish.conf.templatesDir;
      var classTemplate = new JSDOC.JsPlate(templatesDir + 'class.tmpl');
      var membersTemplate = new JSDOC.JsPlate(templatesDir + 'members.tmpl');
      var classTreeTemplate = new JSDOC.JsPlate(templatesDir + 'classtree.tmpl');
      var fileListTemplate = new JSDOC.JsPlate(templatesDir + 'filelist.tmpl');
      var annotatedTemplate = new JSDOC.JsPlate(templatesDir + 'annotated.tmpl');
      var namespacesTemplate = new JSDOC.JsPlate(templatesDir +
                                                 'namespaces.tmpl');
    } catch(e) {
      generateError('Couldn\'t create the required templates: ' + e);
      System.exit(1);
    }
  
    // some ustility filters
    function hasNoParent($) {return ($.memberOf == '')}
    function isaFile($) {return ($.is('FILE'))}
    function isaClass($) {return ($.is('CONSTRUCTOR') || $.isNamespace)}
  
    // get an array version of the symbolset, useful for filtering
    var symbols = symbolSet.toArray();
  
    // create the hilited source code files
    if (false) {
      var files = JSDOC.opt.srcFiles;
      for (var i = 0, l = files.length; i < l; i++) {
        var file = files[i];
        makeSrcFile(file, publish.conf.srcDir);
      }
    }
  
    // get a list of all the classes in the symbolset
    var classes = symbols.filter(isaClass).sort(makeSortby('alias'));
  
    // create each of the class pages
    for (var i = 0, l = classes.length; i < l; i++) {
      var symbol = classes[i];
      g_unknownTypes = { };
  
      symbol.events = symbol.getEvents();   // 1 order matters
      symbol.methods = symbol.getMethods(); // 2
  
      print('Generating docs for: ' + symbol.alias);
      // Comment these lines in to see what data is available to the templates.
      //print('----------------------------------------------------------------');
      //dumpObject(symbol, 5);
  
      // <a href='symbol.source'>symbol.filename</a>
      symbol.source = symbol.srcFile;    // This is used as a link to the source
      symbol.filename = symbol.srcFile;  // This is display as the link.
  
      var output = '';
      output = classTemplate.process(symbol);
  
      IO.saveFile(publish.conf.outDir,
                  (publish.conf.prefix + symbol.alias +
                   '_ref' + publish.conf.ext).toLowerCase(),
                  output);
      IO.saveFile(publish.conf.htmlDir,
                  (publish.conf.prefix + symbol.alias +
                   '_ref.html').toLowerCase(),
                  output);
  
      var output = '';
      output = membersTemplate.process(symbol);
      IO.saveFile(publish.conf.outDir,
                  (publish.conf.prefix + symbol.alias +
                   '_members' + publish.conf.ext).toLowerCase(),
                  output);
      IO.saveFile(publish.conf.htmlDir,
                  (publish.conf.prefix + symbol.alias +
                   '_members.html').toLowerCase(),
                  output);
    }
  
    var classTree = classTreeTemplate.process(classes);
    IO.saveFile(publish.conf.outDir, 'classtree.html', classTree);
  
    var fileList = fileListTemplate.process(symbols);
    IO.saveFile(publish.conf.outDir, 'filelist.html', fileList);
  
    var annotated = annotatedTemplate.process(classes);
    IO.saveFile(publish.conf.outDir, 'annotated' + publish.conf.ext, annotated);
    IO.saveFile(publish.conf.htmlDir, 'annotated.html', annotated);
  
    var namespaces = namespacesTemplate.process(classes);
    IO.saveFile(publish.conf.outDir, 'namespaces' + publish.conf.ext, namespaces);
    IO.saveFile(publish.conf.htmlDir, 'namespaces.html', namespaces);
  
    if (g_numErrors > 0) {
      print('Num Errors: ' + g_numErrors);
      System.exit(1);
    }
  }
  
  
  /**
   *  Gets just the first sentence (up to a full stop).
   *  Should not break on dotted variable names.
   *  @param {string} desc Description to extract summary from.
   *  @return {string} summary.
   */
  function summarize(desc) {
    if (typeof desc != 'undefined')
      return desc.match(/([\w\W]+?\.)[^a-z0-9_$]/i) ? RegExp.$1 : desc;
  }
  
  /**
   * Makes a symbol sorter by some attribute.
   * @param {string} attribute to sort by.
   * @return {number} sorter result.
   */
  function makeSortby(attribute) {
    return function(a, b) {
      if (a[attribute] != undefined && b[attribute] != undefined) {
        a = a[attribute].toLowerCase();
        b = b[attribute].toLowerCase();
        if (a < b) return -1;
        if (a > b) return 1;
        return 0;
      }
    }
  }
  
  /**
   * Pull in the contents of an external file at the given path.
   * @param {string} path Path of file relative to template directory.
   * @return {string} contents of file.
   */
  function include(path) {
    var path = publish.conf.templatesDir + path;
    return IO.readFile(path);
  }
  
  /**
   * Turns a raw source file into a code-hilited page in the docs.
   * @param {string} path Path to source.
   * @param {string} srcDir path to place to store hilited page.
   * @param {string} opt_name to name output file.
   *
   */
  function makeSrcFile(path, srcDir, opt_name) {
    if (JSDOC.opt.s) return;
  
    if (!opt_name) {
      opt_name = path.replace(/\.\.?[\\\/]/g, '').replace(/[\\\/]/g, '_');
      opt_name = opt_name.replace(/\:/g, '_');
    }
  
    var src = {path: path, name: opt_name, charset: IO.encoding, hilited: ''};
  
    if (defined(JSDOC.PluginManager)) {
      JSDOC.PluginManager.run('onPublishSrc', src);
    }
  
    if (src.hilited) {
      IO.saveFile(srcDir, opt_name + publish.conf.ext, src.hilited);
    }
  }
  
  /**
   * Builds output for displaying function parameters.
   * @param {Array} params Array of function params.
   * @return {string} string in format '(param1, param2)'.
   */
  function makeSignature(params) {
    if (!params) return '()';
    var signature = '(' +
    params.filter(
      function($) {
        return $.name.indexOf('.') == -1; // don't show config params in signature
      }
    ).map(
      function($) {
        return $.name;
      }
    ).join(', ') + ')';
    return signature;
  }
  
  /**
   * Find symbol {@link ...} strings in text and turn into html links.
   * @param {string} str String to modify.
   * @return {string} modifed string.
   */
  function resolveLinks(str) {
    str = str.replace(/\{@link ([^} ]+) ?\}/gi,
      function(match, symbolName) {
        return new Link().toSymbol(symbolName);
      }
    );
  
    return str;
  }
  
  /**
   * Make link from symbol.
   * @param {Object} symbol Symbol from class database.
   * @param {string} opt_extra extra suffix to add before '.html'.
   * @return {string} url to symbol.
   */
  function getLinkToSymbol(symbol, opt_extra) {
    if (symbol.is('CONSTRUCTOR') || symbol.isNamespace) {
      return (g_filePrefix + symbol.alias + (opt_extra || '_ref') +
              '.html').toLowerCase();
    } else {
      var parentSymbol = getSymbol(symbol.memberOf);
      return (g_filePrefix + parentSymbol.alias + (opt_extra || '_ref') +
              '.html').toLowerCase() + '#' + symbol.name;
    }
  }
  
  /**
   * Given a class alias, returns a link to its reference page
   * @param {string} classAlias Fully qualified name of class.
   * @return {string} url to class.
   */
  function getLinkToClassByAlias(classAlias) {
    var symbol = getSymbol(classAlias);
    if (!symbol) {
      throw Error('no documentation for "' + classAlias + '"');
    }
    return getLinkToSymbol(symbol);
  }
  
  /**
   * Given a class alias, returns a link to its member reference page
   * @param {string} classAlias Fully qualified name of class.
   * @return {string} url to class in members file.
   */
  function getLinkToClassMembersByAlias(classAlias) {
    var symbol = getSymbol(classAlias);
    return getLinkToSymbol(symbol, '_members');
  }
  
  /**
   * Given a class alias like o3djs.namespace.function returns an HTML string
   * with a link to each part (o3djs, namespace, function)
   * @param {string} classAlias Fully qualified alias of class.
   * @param {string} opt_cssClassId css class Id to put in class="" instead links.
   * @return {string} html with links to each class and parent.
   */
  function getHierarchicalLinksToClassByAlias(classAlias, opt_cssClassId) {
    var parts = classAlias.split('.');
    var name = '';
    var html = '';
    var delim = '';
    var classId = '';
    if (opt_cssClassId) {
      classId = ' class="' + opt_cssClassId + '"';
    }
    for (var pp = 0; pp < parts.length; ++pp) {
      var part = parts[pp];
      name = name + delim + part;
      link = getLinkToClassByAlias(name);
      html = html + delim + '<a' + classId +
             ' href="' + link + '">' + part + '</a>';
      delim = '.';
    }
    return html;
  }
  
  /**
   * Dumps a javascript object.
   *
   * @param {Object} obj Object to dump.
   * @param {number} depth Depth to dump (0 = forever).
   * @param {string} opt_prefix Prefix to put before each line.
   */
  function dumpObject(obj, depth, opt_prefix) {
    opt_prefix = opt_prefix || '';
    --depth;
    for (var prop in obj) {
      if (typeof obj[prop] != 'function') {
        dumpWithPrefix(prop + ' : ' + obj[prop], opt_prefix);
        if (depth != 0) {
          dumpObject(obj[prop], depth, opt_prefix + '    ');
        }
      }
    }
  }
  
  /**
   * Dumps a string, putting a prefix before each line
   * @param {string} str String to dump.
   * @param {string} prefix Prefix to put before each line.
   */
  function dumpWithPrefix(str, prefix) {
    var parts = str.split('\n');
    for (var pp = 0; pp < parts.length; ++pp) {
      print(prefix + parts[pp]);
    }
  }
  
  /**
   * gets the type of a property.
   * @param {!object} property Property object.
   * @return {string} type of property.
   */
  function getPropertyType(property) {
    if (property.type.length > 0) {
      return property.type;
    } else {
      var tag = property.comment.getTag('type');
      if (tag.length > 0) {
        return tag[0].type;
      } else {
        return 'undefined';
      }
    }
  }
  
  /**
   * Converts [ to [[] for ezt files.
   * Also converts '\n\n' to <br/></br>
   * @param {string} str to sanitize.
   * @return {string} Sanitized string.
   */
  function sanitizeForEZT(str) {
    return str.replace(/\[/g, '[[]').replace(/\n\n/g, '<br/><br/>');
  }
  
  /**
   * Check if string starts with another string.
   * @param {string} str String to check.
   * @param {string} prefix Prefix to check for.
   * @return {boolean} True if str starts with prefix.
   */
  function startsWith(str, prefix) {
    return str.substring(0, prefix.length) === prefix;
  }
  
  /**
   * Check if string ends with another string.
   * @param {string} str String to check.
   * @param {string} suffix Suffix to check for.
   * @return {boolean} True if str ends with suffix.
   */
  function endsWith(str, suffix) {
    return str.substring(str.length - suffix.length) === suffix;
  }
  
  /**
   * Converts a camelCase name to underscore as in TypeOfFruit becomes
   * type_of_fruit.
   * @param {string} str CamelCase string.
   * @return {string} underscorified str.
   */
  function camelCaseToUnderscore(str) {
    function toUnderscore(match) {
      return '_' + match.toLowerCase();
    }
    return str[0].toLowerCase() +
        str.substring(1).replace(/[A-Z]/g, toUnderscore);
  }
  
  /**
   * Prints a warning about an unknown type only once.
   * @param {string} place Use to print error message if type not found.
   * @param {string} type Type specification.
   */
  function reportUnknownType(place, type) {
    if (!g_unknownTypes[type]) {
      g_unknownTypes[type] = true;
      generatePlaceError (place, 'reference to unknown type: "' + type + '"');
    }
  }
  
  /**
   * Gets index of closing character.
   * @param {string} str string to search.
   * @param {number} startIndex index to start searching at. Must be an opening
   *      character.
   * @return {number} Index of closing character or (-1) if not found.
   */
  function getIndexOfClosingCharacter(str, startIndex) {
    var openCloseMap = {
      '(': ')',
      '<': '>',
      '[': ']',
      '{': '}'};
    var closeMap = {
      ')': true,
      '>': true,
      ']': true,
      '}': true,
    };
    var stack = [];
    if (!openCloseMap[str[startIndex]]) {
      throw 'startIndex does not point to opening character.';
    }
    var endIndex = str.length;
    while (startIndex < endIndex) {
      var c = str[startIndex];
      var closing = openCloseMap[c];
      if (closing) {
        stack.unshift(closing);
      } else {
        closing = closeMap[c];
        if (closing) {
          var expect = stack.shift()
          if (c != expect) {
            return -1;
          }
          if (stack.length == 0) {
            return startIndex;
          }
        }
      }
      ++startIndex;
    }
    return -1;
  }
  
  /**
   * Make's a name by concatenating strings.
   * @param {...[string]} strings to concatenate.
   * @return {string} Concatenated string.
   */
  function makeName() {
    var str = '';
    for (var ii = 0; ii < arguments.length; ++ii) {
      if (str) {
        str += '.';
      }
      str += arguments[ii];
    }
    return str;
  }
  
  /**
   * Generates an error msg.
   * @param {string} msg.
   */
  function generateError(msg) {
    ++g_numErrors;
    print('ERROR: ' + msg);
  }
  
  /**
   * Generates an error msg.
   * @param {string} place Use to print error message.
   * @param {string} msg.
   */
  function generatePlaceError(place, msg) {
    generateError(place + ': ' + msg);
  }
  
  /**
   * Converts a reference to a single JSDOC type specification to an html link.
   * @param {string} place Use to print error message if type not found.
   * @param {string} str to linkify.
   * @return {string} linkified string.
   */
  function linkifySingleType(place, type) {
    var not = '';
    var equals = '';
    // Remove ! if it exists.
    if (type[0] == '!') {
      not = '!'
      type = type.substring(1);
    }
    if (endsWith(type, '=')) {
      equals = '=';
      type = type.substring(0, type.length - 1);
    }
  
    var link = type;
  
    // Check for array wrapper.
    if (startsWith(type, 'Array.<')) {
      var closingAngle = getIndexOfClosingCharacter(type, 6);
      if (closingAngle < 0) {
        generatePlaceError(place, 'Unmatched "<" in Array type : ' + type);
      } else {
        link = 'Array.&lt;' +
          linkifySingleType(place, type.substring(7, closingAngle)) + '>';
      }
    } else if (startsWith(type, 'function(')) {
      var closingParen = getIndexOfClosingCharacter(type, 8);
      if (closingParen < 0) {
        generatePlaceError(place, 'Unmatched "(" in function type : ' + type);
      } else {
        var end = type.substring(closingParen + 1);
        if (!startsWith(end, ': ')) {
          generatePlaceError(place,
              'Malformed return specification on function. Must be' +
              ' "function(args): type" including the space after the colon.');
        } else {
          var output = '';
          var argsStr = type.substring(9, closingParen);
          if (argsStr) {
            var args = argsStr.split(/ *, */);
            for (var ii = 0; ii < args.length; ++ii) {
              if (ii > 0) {
                output += ', ';
              }
              output += linkifyTypeSpec(place, args[ii]);
            }
          }
          link = 'function(' + output + '): ' +
                 linkifyTypeSpec(place, end.substring(2));
        }
      }
    } else if (type.indexOf(':') >= 0) { // check for records.
      if (type.indexOf('::') >= 0) { // check for CPP scope
        generatePlaceError(place,
                           'CPP "::" scope operator found for type "' + type +
                           '" must be Javascript "." scope operator.');
      } else {
        var elements = type.split(/\s*,\s*/);
        var output = '{';
        for (var ii = 0; ii < elements.length; ++ii) {
          if (ii > 0) {
            output += ', ';
          }
          var element = elements[ii];
          var colon = element.indexOf(': ');
          if (colon < 0) {
            generatePlaceError(place,
                               'Malformed record specification. Format must be ' +
                               '{id1: type1, id2: type2, ...}.');
            output += element;
          } else {
            var name = element.substring(0, colon);
            var subType = element.substring(colon + 2);
            output += name + ':&nbsp;' + linkifyTypeSpec(place, subType)
          }
        }
        link = output + '}';
      }
    } else {
      var symbol = getSymbol(type);
      if (symbol) {
        link = '<a class="el" href="' + getLinkToSymbol(symbol) + '">' +
            type + '</a>';
      } else {
        // See if the symbol is a property or field.
        var period = type.lastIndexOf('.');
        if (period >= 0 && type != '...') {
          var subType = type.substring(0, period);
          symbol = getSymbol(subType);
          if (symbol) {
            var field = type.substring(period + 1);
            link = '<a class="el" href="' + getLinkToSymbol(symbol) + '#' +
                field + '">' +  type + '</a>';
          } else {
            if (startsWith(type, 'o3d.')) {
              // TODO(gman): remove this hack, make nixysa generate JSDOC js
              //     files instead of C++ headers and pass those into
              //     jsdoctoolkit.
              reportUnknownType(place, type);
              link = '<a class="el" href="../classo3d_1_1_' +
                  camelCaseToUnderscore(type.substring(4)) + '.html">' +
                  type + '</a>';
            } else {
              if (subType[0] == '?') {
                subType = subType.substring(1);
              }
              if (!g_validJSDOCTypes[subType]) {
                reportUnknownType(place, type);
              }
            }
          }
        }
      }
    }
  
    return not + link + equals;
  }
  
  /**
   * Fix function specs.
   * The jsdoctoolkit wrongly converts ',' to | as in 'function(a, b)' to
   * 'function(a|b)' and '{id1: type1, id2: type2}' to '{id1: type1|id2: type2}'
   * so we need to put those back here (or fix jsdoctoolkit, the proper solution).
   * @param {string} str JSDOC type specification string .
   * @return {string} str with '|' converted back to ', ' unless the specification
   *     starts with '(' and ends with ')'. That's not a very good check beacuse
   *     you could 'function((string|number)) and this would fail to do the right
   *     thing.
   *
   */
  function fixSpecCommas(str) {
    // TODO: This is really a complete hack and we should fix the
    // jsdoctoolkit.
    if (startsWith(str, '(') && endsWith(str, ')')) {
      return str;
    } else {
      return str.replace(/\|/g, ', ');
    }
  }
  
  /**
   * Converts a JSDOC type specification into html links. For example
   * '(!o3djs.math.Vector3|!O3D.math.Vector4)' would change to
   * '(!<a href="??">o3djs.math.Vector3</a>
   * |!<a href="??">o3djs.math.Vector4</a>)'.
   * @param {string} place Use to print error message if type not found.
   * @param {string} str to linkify.
   * @return {string} linkified string.
   */
  function linkifyTypeSpec(place, str) {
    var output = '';
    if (str) {
      var fixed = fixSpecCommas(str);
      // TODO: needs to split outside of parens and angle brackets.
      if (startsWith(fixed, '(') && endsWith(fixed, ')')) {
        var types = fixed.substring(1, fixed.length - 1).split('|');
        output += '(';
        for (var tt = 0; tt < types.length; ++tt) {
          if (tt > 0) {
            output += '|';
          }
          output += linkifySingleType(place, types[tt]);
        }
        output += ')';
      } else {
        output += linkifySingleType(place, fixed);
      }
    } else {
      generatePlaceError(place, 'missing type specification (' + str + ')');
    }
    return output;
  }
  
  /**
   * Same as linkifyTypeSpec but allows str to be undefined or ''.
   * @param {string} place Use to print error message if type not found.
   * @param {string} str to linkify.
   * @return {string} linkified string.
   */
  function linkifyTypeSpecForReturn(place, str) {
    if (str) {
      return linkifyTypeSpec(place, str);
    }
    return '';
  }
  
  /**
   * Gets a symbol for a type.
   * This is here mostly for debugging so you can insert a print before or after
   * each call to g_symbolSet.getSymbol.
   * @param {string} type fully qualified type.
   * @return {Symbol} The symbol object for the type or null if not found.
   */
  function getSymbol(type) {
    return g_symbolSet.getSymbol(type);
  }
  
  /**
   * Gets the source path for a symbol starting from 'o3djs/'
   * @param {!Symbol} symbol The symbol to get the source name for.
   * @return {string} The name of the source file.
   */
  function getSourcePath(symbol) {
    var path = symbol.srcFile.replace(/\\/g, '/');
    var index = path.indexOf('/o3djs/');
    return path.substring(index + 1);
  }
  