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
21var API_TEMPLATE = "template/api_template.html";
22var WEBKIT_PATH = "../../../../third_party/WebKit";
23var SCHEMA = "../api/extension_api.json";
24var DEVTOOLS_SCHEMA = WEBKIT_PATH +
25  "/Source/WebCore/inspector/front-end/ExtensionAPISchema.json";
26var USE_DEVTOOLS_SCHEMA =
27  /\.webInspector[^/]*\.html/.test(location.pathname);
28var API_MODULE_PREFIX = USE_DEVTOOLS_SCHEMA ? "" : "chrome.";
29var SAMPLES = "samples.json";
30var REQUEST_TIMEOUT = 2000;
31
32function staticResource(name) { return "static/" + name + ".html"; }
33
34// Base name of this page. (i.e. "tabs", "overview", etc...).
35var pageBase;
36
37// Data to feed as context into the template.
38var pageData = {};
39
40// The full extension api schema
41var schema;
42
43// List of Chrome extension samples.
44var samples;
45
46// Mappings of api calls to URLs
47var apiMapping;
48
49// The current module for this page (if this page is an api module);
50var module;
51
52// Mapping from typeId to module.
53var typeModule = {};
54
55// Auto-created page name as default
56var pageName;
57
58// If this page is an apiModule, the name of the api module
59var apiModuleName;
60
61
62// Visits each item in the list in-order. Stops when f returns any truthy
63// value and returns that node.
64Array.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|.
72function 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 */
84function 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
104function 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
115function 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
148function 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 */
164function 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
205function 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
267function 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
281function serializePage() {
282 removeJsTemplateAttributes(document);
283 var s = new XMLSerializer();
284 return s.serializeToString(document);
285}
286
287function 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
298function 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.
304function 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
334function 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
342function 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
350function 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
358function getDataFromPageHTML(id) {
359  var node = document.getElementById(id);
360  if (!node)
361    return;
362  return node.innerHTML;
363}
364
365function isArray(type) {
366  return type.type == 'array';
367}
368
369function isFunction(type) {
370  return type.type == 'function';
371}
372
373function getTypeRef(type) {
374  return type["$ref"];
375}
376
377function 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
385function showPageTOC() {
386  return module || getDataFromPageHTML('pageData-showTOC');
387}
388
389function showSideNav() {
390  return getDataFromPageHTML("pageData-showSideNav") != "false";
391}
392
393function 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.
422function 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
446function getTypeRefPage(type) {
447  return typeModule[type.$ref].namespace + ".html";
448}
449
450function 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
460function 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
470function getModuleName() {
471  return API_MODULE_PREFIX + module.namespace;
472}
473
474function getFullyQualifiedFunctionName(scope, func) {
475  return (getObjectName(scope) || getModuleName()) + "." + func.name;
476}
477
478function getObjectName(typeName) {
479  return typeName.charAt(0).toLowerCase() + typeName.substring(1);
480}
481
482function isExperimentalAPIPage() {
483  return (getPageName().indexOf('.experimental.') >= 0 &&
484          getPageName().indexOf('.experimental.*') < 0);
485}
486
487function hasCallback(parameters) {
488  return (parameters.length > 0 &&
489          parameters[parameters.length - 1].type == "function");
490}
491
492function getCallbackParameters(parameters) {
493  return parameters[parameters.length - 1];
494}
495
496function getAnchorName(type, name, scope) {
497  return type + "-" + (scope ? scope + "-" : "") + name;
498}
499
500function shouldExpandObject(object) {
501  return (object.type == "object" && object.properties);
502}
503
504function 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
514function 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
536function 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
547function 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