Home | History | Annotate | Download | only in js
      1 // Copyright (c) 2011 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 /**
      6  * @fileoverview This file is the controller for generating extension
      7  * doc pages.
      8  *
      9  * It expects to have available via XHR (relative path):
     10  *   1) API_TEMPLATE which is the main template for the api pages.
     11  *   2) A file located at SCHEMA which is shared with the extension system and
     12  *      defines the methods and events contained in one api.
     13  *   3) (Possibly) A static version of the current page url in /static/. I.e.
     14  *      if called as ../foo.html, it will look for ../static/foo.html.
     15  *
     16  * The "shell" page may have a renderering already contained within it so that
     17  * the docs can be indexed.
     18  *
     19  */
     20 
     21 var API_TEMPLATE = "template/api_template.html";
     22 var WEBKIT_PATH = "../../../../third_party/WebKit";
     23 var SCHEMA = "../api/extension_api.json";
     24 var DEVTOOLS_SCHEMA = WEBKIT_PATH +
     25   "/Source/WebCore/inspector/front-end/ExtensionAPISchema.json";
     26 var USE_DEVTOOLS_SCHEMA =
     27   /\.webInspector[^/]*\.html/.test(location.pathname);
     28 var API_MODULE_PREFIX = USE_DEVTOOLS_SCHEMA ? "" : "chrome.";
     29 var SAMPLES = "samples.json";
     30 var REQUEST_TIMEOUT = 2000;
     31 
     32 function staticResource(name) { return "static/" + name + ".html"; }
     33 
     34 // Base name of this page. (i.e. "tabs", "overview", etc...).
     35 var pageBase;
     36 
     37 // Data to feed as context into the template.
     38 var pageData = {};
     39 
     40 // The full extension api schema
     41 var schema;
     42 
     43 // List of Chrome extension samples.
     44 var samples;
     45 
     46 // Mappings of api calls to URLs
     47 var apiMapping;
     48 
     49 // The current module for this page (if this page is an api module);
     50 var module;
     51 
     52 // Mapping from typeId to module.
     53 var typeModule = {};
     54 
     55 // Auto-created page name as default
     56 var pageName;
     57 
     58 // If this page is an apiModule, the name of the api module
     59 var apiModuleName;
     60 
     61 
     62 // Visits each item in the list in-order. Stops when f returns any truthy
     63 // value and returns that node.
     64 Array.prototype.select = function(f) {
     65   for (var i = 0; i < this.length; i++) {
     66     if (f(this[i], i))
     67       return this[i];
     68   }
     69 }
     70 
     71 // Assigns all keys & values of |obj2| to |obj1|.
     72 function extend(obj, obj2) {
     73   for (var k in obj2) {
     74     obj[k] = obj2[k];
     75   }
     76 }
     77 
     78 /*
     79  * Main entry point for composing the page. It will fetch it's template,
     80  * the extension api, and attempt to fetch the matching static content.
     81  * It will insert the static content, if any, prepare it's pageData then
     82  * render the template from |pageData|.
     83  */
     84 function renderPage() {
     85   // The page name minus the ".html" extension.
     86   pageBase = document.location.href.match(/\/([^\/]*)\.html/)[1];
     87   if (!pageBase) {
     88     alert("Empty page name for: " + document.location.href);
     89     return;
     90   }
     91 
     92   pageName = pageBase.replace(/([A-Z])/g, " $1");
     93   pageName = pageName.substring(0, 1).toUpperCase() + pageName.substring(1);
     94 
     95   // Fetch the api template and insert into the <body>.
     96   fetchContent(API_TEMPLATE, function(templateContent) {
     97     document.getElementsByTagName("body")[0].innerHTML = templateContent;
     98     fetchStatic();
     99   }, function(error) {
    100     alert("Failed to load " + API_TEMPLATE + ". " + error);
    101   });
    102 }
    103 
    104 function fetchStatic() {
    105   // Fetch the static content and insert into the "static" <div>.
    106   fetchContent(staticResource(pageBase), function(overviewContent) {
    107     document.getElementById("static").innerHTML = overviewContent;
    108     fetchSchema();
    109   }, function(error) {
    110     // Not fatal. Some api pages may not have matching static content.
    111     fetchSchema();
    112   });
    113 }
    114 
    115 function fetchSchema() {
    116   // Now the page is composed with the authored content, we fetch the schema
    117   // and populate the templates.
    118   var is_experimental_index = /\/experimental\.html$/.test(location.pathname);
    119 
    120   var schemas_to_retrieve = [];
    121   if (!USE_DEVTOOLS_SCHEMA || is_experimental_index)
    122     schemas_to_retrieve.push(SCHEMA);
    123   if (USE_DEVTOOLS_SCHEMA || is_experimental_index)
    124     schemas_to_retrieve.push(DEVTOOLS_SCHEMA);
    125 
    126   var schemas_retrieved = 0;
    127   schema = [];
    128 
    129   function onSchemaContent(content) {
    130     schema = schema.concat(JSON.parse(content));
    131     if (++schemas_retrieved < schemas_to_retrieve.length)
    132       return;
    133     if (pageName.toLowerCase() == "samples") {
    134       fetchSamples();
    135     } else {
    136       renderTemplate();
    137     }
    138   }
    139 
    140   for (var i = 0; i < schemas_to_retrieve.length; ++i) {
    141     var schema_path = schemas_to_retrieve[i];
    142     fetchContent(schema_path, onSchemaContent, function(error) {
    143       alert("Failed to load " + schema_path);
    144     });
    145   }
    146 }
    147 
    148 function fetchSamples() {
    149   // If we're rendering the samples directory, fetch the samples manifest.
    150   fetchContent(SAMPLES, function(sampleManifest) {
    151     var data = JSON.parse(sampleManifest);
    152     samples = data.samples;
    153     apiMapping = data.api;
    154     renderTemplate();
    155   }, function(error) {
    156     renderTemplate();
    157   });
    158 }
    159 
    160 /**
    161  * Fetches |url| and returns it's text contents from the xhr.responseText in
    162  * onSuccess(content)
    163  */
    164 function fetchContent(url, onSuccess, onError) {
    165   var localUrl = url;
    166   var xhr = new XMLHttpRequest();
    167   var abortTimerId = window.setTimeout(function() {
    168     xhr.abort();
    169     console.log("XHR Timed out");
    170   }, REQUEST_TIMEOUT);
    171 
    172   function handleError(error) {
    173     window.clearTimeout(abortTimerId);
    174     if (onError) {
    175       onError(error);
    176       // Some cases result in multiple error handings. Only fire the callback
    177       // once.
    178       onError = undefined;
    179     }
    180   }
    181 
    182   try {
    183     xhr.onreadystatechange = function(){
    184       if (xhr.readyState == 4) {
    185         if (xhr.status < 300 && xhr.responseText) {
    186           window.clearTimeout(abortTimerId);
    187           onSuccess(xhr.responseText);
    188         } else {
    189           handleError("Failure to fetch content");
    190         }
    191       }
    192     }
    193 
    194     xhr.onerror = handleError;
    195 
    196     xhr.open("GET", url, true);
    197     xhr.send(null);
    198   } catch(e) {
    199     console.log("ex: " + e);
    200     console.error("exception: " + e);
    201     handleError();
    202   }
    203 }
    204 
    205 function renderTemplate() {
    206   schema.forEach(function(mod) {
    207     if (mod.namespace == pageBase) {
    208       // Do not render page for modules which are marked as "nodoc": true.
    209       if (mod.nodoc) {
    210         return;
    211       }
    212       // This page is an api page. Setup types and apiDefinition.
    213       module = mod;
    214       apiModuleName = API_MODULE_PREFIX + module.namespace;
    215       pageData.apiDefinition = module;
    216     }
    217 
    218     if (mod.types) {
    219       mod.types.forEach(function(type) {
    220         typeModule[type.id] = mod;
    221       });
    222     }
    223   });
    224 
    225   /**
    226    * Special pages like the samples gallery may want to modify their template
    227    * data to include additional information.  This hook allows a page template
    228    * to specify code that runs in the context of the api_page_generator.js
    229    * file before the jstemplate is rendered.
    230    *
    231    * To specify such code, the page template should include a script block with
    232    * a type of "text/prerenderjs" containing the code to be executed.  Note that
    233    * linking to an external file is not supported - code must be accessible
    234    * via the script block's innerText property.
    235    *
    236    * Code that is run this way may modify the data sent to jstemplate by
    237    * modifying the window.pageData variable.  This code will also have access
    238    * to any methods declared in the api_page_generator.js file.  The code
    239    * does not need to return any specific value to function.
    240    *
    241    * Note that code specified in this manner will be removed before the
    242    * template is rendered, and will therefore not be exposed to the end user
    243    * in the final rendered template.
    244    */
    245   var preRender = document.querySelector('script[type="text/prerenderjs"]');
    246   if (preRender) {
    247     preRender.parentElement.removeChild(preRender);
    248     eval(preRender.innerText);
    249   }
    250 
    251   // Render to template
    252   var input = new JsEvalContext(pageData);
    253   var output = document.getElementsByTagName("body")[0];
    254   jstProcess(input, output);
    255 
    256   selectCurrentPageOnLeftNav();
    257 
    258   document.title = getPageTitle();
    259   // Show
    260   if (window.postRender)
    261     window.postRender();
    262 
    263   if (parent && parent.done)
    264     parent.done();
    265 }
    266 
    267 function removeJsTemplateAttributes(root) {
    268   var jsattributes = ["jscontent", "jsselect", "jsdisplay", "transclude",
    269                       "jsvalues", "jsvars", "jseval", "jsskip", "jstcache",
    270                       "jsinstance"];
    271 
    272   var nodes = root.getElementsByTagName("*");
    273   for (var i = 0; i < nodes.length; i++) {
    274     var n = nodes[i]
    275     jsattributes.forEach(function(attributeName) {
    276       n.removeAttribute(attributeName);
    277     });
    278   }
    279 }
    280 
    281 function serializePage() {
    282  removeJsTemplateAttributes(document);
    283  var s = new XMLSerializer();
    284  return s.serializeToString(document);
    285 }
    286 
    287 function evalXPathFromNode(expression, node) {
    288   var results = document.evaluate(expression, node, null,
    289       XPathResult.ORDERED_NODE_ITERATOR_TYPE, null);
    290   var retval = [];
    291   while(n = results.iterateNext()) {
    292     retval.push(n);
    293   }
    294 
    295   return retval;
    296 }
    297 
    298 function evalXPathFromId(expression, id) {
    299   return evalXPathFromNode(expression, document.getElementById(id));
    300 }
    301 
    302 // Select the current page on the left nav. Note: if already rendered, this
    303 // will not effect any nodes.
    304 function selectCurrentPageOnLeftNav() {
    305   function finalPathPart(str) {
    306     var pathParts = str.split(/\//);
    307     var lastPart = pathParts[pathParts.length - 1];
    308     return lastPart.split(/\?/)[0];
    309   }
    310 
    311   var pageBase = finalPathPart(document.location.href);
    312 
    313   evalXPathFromId(".//li/a", "gc-toc").select(function(node) {
    314     if (pageBase == finalPathPart(node.href)) {
    315       var parent = node.parentNode;
    316       if (node.firstChild.nodeName == 'DIV') {
    317         node.firstChild.className = "leftNavSelected";
    318       } else {
    319         parent.className = "leftNavSelected";
    320       }
    321       parent.removeChild(node);
    322       parent.insertBefore(node.firstChild, parent.firstChild);
    323       return true;
    324     }
    325   });
    326 }
    327 
    328 /*
    329  * Template Callout Functions
    330  * The jstProcess() will call out to these functions from within the page
    331  * template
    332  */
    333 
    334 function stableAPIs() {
    335   return schema.filter(function(module) {
    336     return !module.nodoc && module.namespace.indexOf("experimental") < 0;
    337   }).map(function(module) {
    338     return module.namespace;
    339   }).sort();
    340 }
    341 
    342 function experimentalAPIs() {
    343   return schema.filter(function(module) {
    344     return !module.nodoc && module.namespace.indexOf("experimental") == 0;
    345   }).map(function(module) {
    346     return module.namespace;
    347   }).sort();
    348 }
    349 
    350 function webInspectorAPIs() {
    351   return schema.filter(function(module) {
    352     return !module.nodoc && module.namespace.indexOf("webInspector.") !== 0;
    353   }).map(function(module) {
    354     return module.namespace;
    355   }).sort();
    356 }
    357 
    358 function getDataFromPageHTML(id) {
    359   var node = document.getElementById(id);
    360   if (!node)
    361     return;
    362   return node.innerHTML;
    363 }
    364 
    365 function isArray(type) {
    366   return type.type == 'array';
    367 }
    368 
    369 function isFunction(type) {
    370   return type.type == 'function';
    371 }
    372 
    373 function getTypeRef(type) {
    374   return type["$ref"];
    375 }
    376 
    377 function getEnumValues(enumList, type) {
    378   if (type === "string") {
    379     enumList = enumList.map(function(e) { return '"' + e + '"'});
    380   }
    381   var retval = enumList.join(', ');
    382   return "[" + retval + "]";
    383 }
    384 
    385 function showPageTOC() {
    386   return module || getDataFromPageHTML('pageData-showTOC');
    387 }
    388 
    389 function showSideNav() {
    390   return getDataFromPageHTML("pageData-showSideNav") != "false";
    391 }
    392 
    393 function getStaticTOC() {
    394   var staticHNodes = evalXPathFromId(".//h2|h3", "static");
    395   var retval = [];
    396   var lastH2;
    397 
    398   staticHNodes.forEach(function(n, i) {
    399     var anchorName = n.id || n.nodeName + "-" + i;
    400     if (!n.id) {
    401       var a = document.createElement('a');
    402       a.name = anchorName;
    403       n.parentNode.insertBefore(a, n);
    404     }
    405     var dataNode = { name: n.innerHTML, href: anchorName };
    406 
    407     if (n.nodeName == "H2") {
    408       retval.push(dataNode);
    409       lastH2 = dataNode;
    410       lastH2.children = [];
    411     } else {
    412       lastH2.children.push(dataNode);
    413     }
    414   });
    415 
    416   return retval;
    417 }
    418 
    419 // This function looks in the description for strings of the form
    420 // "$ref:TYPE_ID" (where TYPE_ID is something like "Tab" or "HistoryItem") and
    421 // substitutes a link to the documentation for that type.
    422 function substituteTypeRefs(description) {
    423   var regexp = /\$ref\:\w+/g;
    424   var matches = description.match(regexp);
    425   if (!matches) {
    426     return description;
    427   }
    428   var result = description;
    429   for (var i = 0; i < matches.length; i++) {
    430     var type = matches[i].split(":")[1];
    431     var page = null;
    432     try {
    433       page = getTypeRefPage({"$ref": type});
    434     } catch (error) {
    435       console.log("substituteTypeRefs couldn't find page for type " + type);
    436       continue;
    437     }
    438     var replacement = "<a href='" + page + "#type-" + type + "'>" + type +
    439                       "</a>";
    440     result = result.replace(matches[i], replacement);
    441   }
    442 
    443   return result;
    444 }
    445 
    446 function getTypeRefPage(type) {
    447   return typeModule[type.$ref].namespace + ".html";
    448 }
    449 
    450 function getPageName() {
    451   var pageDataName = getDataFromPageHTML("pageData-name");
    452   // Allow empty string to be explitly set via pageData.
    453   if (pageDataName == "") {
    454     return pageDataName;
    455   }
    456 
    457   return pageDataName || apiModuleName || pageName;
    458 }
    459 
    460 function getPageTitle() {
    461   var pageName = getPageName();
    462   var pageTitleSuffix = "Google Chrome Extensions - Google Code";
    463   if (pageName == "") {
    464     return pageTitleSuffix;
    465   }
    466 
    467   return pageName + " - " + pageTitleSuffix;
    468 }
    469 
    470 function getModuleName() {
    471   return API_MODULE_PREFIX + module.namespace;
    472 }
    473 
    474 function getFullyQualifiedFunctionName(scope, func) {
    475   return (getObjectName(scope) || getModuleName()) + "." + func.name;
    476 }
    477 
    478 function getObjectName(typeName) {
    479   return typeName.charAt(0).toLowerCase() + typeName.substring(1);
    480 }
    481 
    482 function isExperimentalAPIPage() {
    483   return (getPageName().indexOf('.experimental.') >= 0 &&
    484           getPageName().indexOf('.experimental.*') < 0);
    485 }
    486 
    487 function hasCallback(parameters) {
    488   return (parameters.length > 0 &&
    489           parameters[parameters.length - 1].type == "function");
    490 }
    491 
    492 function getCallbackParameters(parameters) {
    493   return parameters[parameters.length - 1];
    494 }
    495 
    496 function getAnchorName(type, name, scope) {
    497   return type + "-" + (scope ? scope + "-" : "") + name;
    498 }
    499 
    500 function shouldExpandObject(object) {
    501   return (object.type == "object" && object.properties);
    502 }
    503 
    504 function getPropertyListFromObject(object) {
    505   var propertyList = [];
    506   for (var p in object.properties) {
    507     var prop = object.properties[p];
    508     prop.name = p;
    509     propertyList.push(prop);
    510   }
    511   return propertyList;
    512 }
    513 
    514 function getTypeName(schema) {
    515   if (schema.$ref)
    516     return schema.$ref;
    517 
    518   if (schema.choices) {
    519     var typeNames = [];
    520     schema.choices.forEach(function(c) {
    521       typeNames.push(getTypeName(c));
    522     });
    523 
    524     return typeNames.join(" or ");
    525   }
    526 
    527   if (schema.type == "array")
    528     return "array of " + getTypeName(schema.items);
    529 
    530   if (schema.isInstanceOf)
    531     return schema.isInstanceOf;
    532 
    533   return schema.type;
    534 }
    535 
    536 function getSignatureString(parameters) {
    537   if (!parameters)
    538     return "";
    539   var retval = [];
    540   parameters.forEach(function(param, i) {
    541     retval.push(getTypeName(param) + " " + param.name);
    542   });
    543 
    544   return retval.join(", ");
    545 }
    546 
    547 function sortByName(a, b) {
    548   if (a.name < b.name) {
    549     return -1;
    550   }
    551   if (a.name > b.name) {
    552     return 1;
    553   }
    554   return 0;
    555 }
    556