// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview This file is the controller for generating extension
* doc pages.
*
* It expects to have available via XHR (relative path):
* 1) API_TEMPLATE which is the main template for the api pages.
* 2) A file located at SCHEMA which is shared with the extension system and
* defines the methods and events contained in one api.
* 3) (Possibly) A static version of the current page url in /static/. I.e.
* if called as ../foo.html, it will look for ../static/foo.html.
*
* The "shell" page may have a renderering already contained within it so that
* the docs can be indexed.
*
*/
var API_TEMPLATE = 'template/api_template.html';
var SCHEMA = '../api/extension_api.json';
var DEVTOOLS_SCHEMA = '../api/devtools_api.json';
var USE_DEVTOOLS_SCHEMA =
/\.devtools[^/]*\.html/.test(location.pathname);
var API_MODULE_PREFIX = 'chrome.';
var SAMPLES = 'samples.json';
var REQUEST_TIMEOUT = 2000;
function staticResource(name) { return 'static/' + name + '.html'; }
// Base name of this page. (i.e. 'tabs', 'overview', etc...).
var pageBase;
// Data to feed as context into the template.
var pageData = {};
// The full extension api schema
var schema;
// List of Chrome extension samples.
var samples;
// Mappings of api calls to URLs
var apiMapping;
// The current module for this page (if this page is an api module);
var module;
// Mapping from typeId to module.
var typeModule = {};
// Auto-created page name as default
var pageName;
// If this page is an apiModule, the name of the api module
var apiModuleName;
// Visits each item in the list in-order. Stops when f returns any truthy
// value and returns that node.
Array.prototype.select = function(f) {
for (var i = 0; i < this.length; i++) {
if (f(this[i], i))
return this[i];
}
}
// Assigns all keys & values of |obj2| to |obj1|.
function extend(obj, obj2) {
for (var k in obj2) {
obj[k] = obj2[k];
}
}
/*
* Main entry point for composing the page. It will fetch it's template,
* the extension api, and attempt to fetch the matching static content.
* It will insert the static content, if any, prepare it's pageData then
* render the template from |pageData|.
*/
function renderPage() {
// The page name minus the '.html' extension.
pageBase = document.location.href.match(/\/([^\/]*)\.html/)[1];
if (!pageBase) {
alert('Empty page name for: ' + document.location.href);
return;
}
pageName = pageBase.replace(/([A-Z])/g, ' $1');
pageName = pageName.substring(0, 1).toUpperCase() + pageName.substring(1);
// Fetch the api template and insert into the
.
fetchContent(API_TEMPLATE, function(templateContent) {
document.getElementsByTagName('body')[0].innerHTML = templateContent;
fetchStatic();
}, function(error) {
alert('Failed to load ' + API_TEMPLATE + '. ' + error);
});
}
function fetchStatic() {
// Fetch the static content and insert into the 'static' .
fetchContent(staticResource(pageBase), function(overviewContent) {
document.getElementById('static').innerHTML = overviewContent;
fetchSchema();
}, function(error) {
// Not fatal. Some api pages may not have matching static content.
fetchSchema();
});
}
function fetchSchema() {
// Now the page is composed with the authored content, we fetch the schema
// and populate the templates.
var is_experimental_index = /\/experimental\.html$/.test(location.pathname);
var schemas_to_retrieve = [];
if (!USE_DEVTOOLS_SCHEMA || is_experimental_index)
schemas_to_retrieve.push(SCHEMA);
if (USE_DEVTOOLS_SCHEMA || is_experimental_index)
schemas_to_retrieve.push(DEVTOOLS_SCHEMA);
var schemas_retrieved = 0;
schema = [];
function onSchemaContent(content) {
schema = schema.concat(JSON.parse(content));
if (++schemas_retrieved < schemas_to_retrieve.length)
return;
if (pageName.toLowerCase() == 'samples') {
fetchSamples();
} else {
renderTemplate();
}
}
for (var i = 0; i < schemas_to_retrieve.length; ++i) {
var schema_path = schemas_to_retrieve[i];
fetchContent(schema_path, onSchemaContent, function(error) {
alert('Failed to load ' + schema_path);
});
}
}
function fetchSamples() {
// If we're rendering the samples directory, fetch the samples manifest.
fetchContent(SAMPLES, function(sampleManifest) {
var data = JSON.parse(sampleManifest);
samples = data.samples;
apiMapping = data.api;
renderTemplate();
}, function(error) {
renderTemplate();
});
}
/**
* Fetches |url| and returns it's text contents from the xhr.responseText in
* onSuccess(content)
*/
function fetchContent(url, onSuccess, onError) {
var localUrl = url;
var xhr = new XMLHttpRequest();
var abortTimerId = window.setTimeout(function() {
xhr.abort();
console.log('XHR Timed out');
}, REQUEST_TIMEOUT);
function handleError(error) {
window.clearTimeout(abortTimerId);
if (onError) {
onError(error);
// Some cases result in multiple error handings. Only fire the callback
// once.
onError = undefined;
}
}
try {
xhr.onreadystatechange = function(){
if (xhr.readyState == 4) {
if (xhr.status < 300 && xhr.responseText) {
window.clearTimeout(abortTimerId);
onSuccess(xhr.responseText);
} else {
handleError('Failure to fetch content');
}
}
}
xhr.onerror = handleError;
xhr.open('GET', url, true);
xhr.send(null);
} catch(e) {
console.log('ex: ' + e);
console.error('exception: ' + e);
handleError();
}
}
function renderTemplate() {
schema.forEach(function(mod) {
if (mod.namespace == pageBase) {
// Do not render page for modules which are marked as "nodoc": true.
if (mod.nodoc) {
return;
}
// This page is an api page. Setup types and apiDefinition.
module = mod;
apiModuleName = API_MODULE_PREFIX + module.namespace;
pageData.apiDefinition = module;
}
if (mod.types) {
mod.types.forEach(function(type) {
typeModule[type.id] = mod;
});
}
});
/**
* Special pages like the samples gallery may want to modify their template
* data to include additional information. This hook allows a page template
* to specify code that runs in the context of the api_page_generator.js
* file before the jstemplate is rendered.
*
* To specify such code, the page template should include a script block with
* a type of "text/prerenderjs" containing the code to be executed. Note that
* linking to an external file is not supported - code must be accessible
* via the script block's innerText property.
*
* Code that is run this way may modify the data sent to jstemplate by
* modifying the window.pageData variable. This code will also have access
* to any methods declared in the api_page_generator.js file. The code
* does not need to return any specific value to function.
*
* Note that code specified in this manner will be removed before the
* template is rendered, and will therefore not be exposed to the end user
* in the final rendered template.
*/
var preRender = document.querySelectorAll('script[type="text/prerenderjs"]');
for (var i = 0; i < preRender.length; i++) {
preRender[i].parentElement.removeChild(preRender[i]);
eval(preRender[i].innerText);
}
// Render to template
var input = new JsEvalContext(pageData);
var output = document.getElementsByTagName('body')[0];
jstProcess(input, output);
selectCurrentPageOnLeftNav();
// Set a `meta` description if the page we're currently generating has a
// module name.
// TODO(mkwst): Come up with something clever for the other types of pages.
if (getModuleName()) {
var m = document.createElement('meta');
var desc = 'Documentation for the ' + getModuleName() +
' module, which is part of the Google Chrome ' +
' extension APIs.';
m.setAttribute('name', 'description');
m.setAttribute('content', desc);
document.head.appendChild(m);
}
document.title = getPageTitle();
// Show
if (window.postRender)
window.postRender();
if (parent && parent.done)
parent.done();
}
function removeJsTemplateAttributes(root) {
var jsattributes = ['jscontent', 'jsselect', 'jsdisplay', 'transclude',
'jsvalues', 'jsvars', 'jseval', 'jsskip', 'jstcache',
'jsinstance'];
var nodes = root.getElementsByTagName('*');
for (var i = 0; i < nodes.length; i++) {
var n = nodes[i]
jsattributes.forEach(function(attributeName) {
n.removeAttribute(attributeName);
});
}
}
function serializePage() {
removeJsTemplateAttributes(document);
var s = new XMLSerializer();
return s.serializeToString(document);
}
function evalXPathFromNode(expression, node) {
var results = document.evaluate(expression, node, null,
XPathResult.ORDERED_NODE_ITERATOR_TYPE, null);
var retval = [];
while(n = results.iterateNext()) {
retval.push(n);
}
return retval;
}
function evalXPathFromId(expression, id) {
return evalXPathFromNode(expression, document.getElementById(id));
}
// Select the current page on the left nav. Note: if already rendered, this
// will not effect any nodes.
function selectCurrentPageOnLeftNav() {
function finalPathPart(str) {
var pathParts = str.split(/\//);
var lastPart = pathParts[pathParts.length - 1];
return lastPart.split(/\?/)[0];
}
var pageBase = finalPathPart(document.location.href);
evalXPathFromId('.//li/a', 'gc-toc').select(function(node) {
if (pageBase == finalPathPart(node.href)) {
var parent = node.parentNode;
if (node.firstChild.nodeName == 'DIV') {
node.firstChild.className = 'leftNavSelected';
} else {
parent.className = 'leftNavSelected';
}
parent.removeChild(node);
parent.insertBefore(node.firstChild, parent.firstChild);
return true;
}
});
}
/*
* Template Callout Functions
* The jstProcess() will call out to these functions from within the page
* template
*/
function stableAPIs() {
return schema.filter(function(module) {
return !module.nodoc && module.namespace.indexOf('experimental') < 0;
}).map(function(module) {
return module.namespace;
}).sort();
}
function experimentalAPIs() {
return schema.filter(function(module) {
return !module.nodoc && module.namespace.indexOf('experimental') == 0;
}).map(function(module) {
return module.namespace;
}).sort();
}
function devtoolsAPIs() {
return schema.filter(function(module) {
return !module.nodoc && module.namespace.indexOf('devtools.') !== 0;
}).map(function(module) {
return module.namespace;
}).sort();
}
function getDataFromPageHTML(id) {
var node = document.getElementById(id);
if (!node)
return;
return node.innerHTML;
}
function isArray(type) {
return type.type == 'array';
}
function isFunction(type) {
return type.type == 'function';
}
function getTypeRef(type) {
return type['$ref'];
}
function getEnumValues(enumList, type) {
if (type === 'string') {
enumList = enumList.map(function(e) { return '"' + e + '"'});
}
var retval = enumList.join(', ');
return '[' + retval + ']';
}
function showPageTOC() {
return module || getDataFromPageHTML('pageData-showTOC');
}
function showSideNav() {
return getDataFromPageHTML('pageData-showSideNav') != 'false';
}
function getStaticTOC() {
var staticHNodes = evalXPathFromId('.//h2|h3', 'static');
var retval = [];
var lastH2;
staticHNodes.forEach(function(n, i) {
var anchorName = n.id || n.nodeName + '-' + i;
if (!n.id) {
var a = document.createElement('a');
a.name = anchorName;
n.parentNode.insertBefore(a, n);
}
var dataNode = { name: n.innerHTML, href: anchorName };
if (n.nodeName == 'H2') {
retval.push(dataNode);
lastH2 = dataNode;
lastH2.children = [];
} else {
lastH2.children.push(dataNode);
}
});
return retval;
}
// This function looks in the description for strings of the form
// "$ref:TYPE_ID" (where TYPE_ID is something like "Tab" or "HistoryItem") and
// substitutes a link to the documentation for that type.
function substituteTypeRefs(description) {
var regexp = /\$ref\:\w+/g;
var matches = description.match(regexp);
if (!matches) {
return description;
}
var result = description;
for (var i = 0; i < matches.length; i++) {
var type = matches[i].split(':')[1];
var page = null;
try {
page = getTypeRefPage({'$ref': type});
} catch (error) {
console.log('substituteTypeRefs couldn\'t find page for type ' + type);
continue;
}
var replacement = '
' + type +
'';
result = result.replace(matches[i], replacement);
}
return result;
}
function getTypeRefPage(type) {
return typeModule[type.$ref].namespace + '.html';
}
function getPageName() {
var pageDataName = getDataFromPageHTML('pageData-name');
// Allow empty string to be explitly set via pageData.
if (pageDataName == '') {
return pageDataName;
}
return pageDataName || apiModuleName || pageName;
}
function getPageTitle() {
var pageName = getPageName();
var pageTitleSuffix = 'Google Chrome Extensions - Google Code';
if (pageName == '') {
return pageTitleSuffix;
}
return pageName + ' - ' + pageTitleSuffix;
}
function getModuleName() {
return (module && typeof module.namespace) ?
API_MODULE_PREFIX + module.namespace : '';
}
function getFullyQualifiedFunctionName(scope, func) {
return (getObjectName(scope) || getModuleName()) + '.' + func.name;
}
function getObjectName(typeName) {
return typeName.charAt(0).toLowerCase() + typeName.substring(1);
}
function isExperimentalAPIPage() {
return (getPageName().indexOf('.experimental.') >= 0 &&
getPageName().indexOf('.experimental.*') < 0);
}
function hasCallback(parameters) {
return (parameters.length > 0 &&
parameters[parameters.length - 1].type == 'function');
}
function getCallbackParameters(parameters) {
return parameters[parameters.length - 1];
}
function getAnchorName(type, name, scope) {
return type + '-' + (scope ? scope + '-' : '') + name;
}
function shouldExpandObject(object) {
return (object.type == 'object' && object.properties) ||
(object.type == 'array' && object.items && object.items.properties);
}
function getPropertyListFromObject(object) {
var propertyList = [];
var properties = object.properties;
if (!properties && object.type === 'array' && object.items) {
properties = object.items.properties;
}
for (var p in properties) {
var prop = properties[p];
// Do not render properties marked as "nodoc": true.
if (prop.nodoc) {
continue;
}
prop.name = p;
propertyList.push(prop);
}
return propertyList;
}
function getTypeName(schema) {
if (schema.$ref)
return schema.$ref;
if (schema.choices) {
var typeNames = [];
schema.choices.forEach(function(c) {
typeNames.push(getTypeName(c));
});
return typeNames.join(' or ');
}
if (schema.type == 'array')
return 'array of ' + getTypeName(schema.items);
if (schema.isInstanceOf)
return schema.isInstanceOf;
return schema.type;
}
function getSignatureString(parameters) {
if (!parameters)
return '';
var retval = [];
parameters.forEach(function(param, i) {
retval.push(getTypeName(param) + ' ' + param.name);
});
return retval.join(', ');
}
function getOptionalSignatureSubstring(parameters) {
if (!parameters)
return '';
return ', ' + getSignatureString(parameters);
}
function sortByName(a, b) {
if (a.name < b.name) {
return -1;
}
if (a.name > b.name) {
return 1;
}
return 0;
}