diff options
Diffstat (limited to 'chrome/common/extensions/docs/js/api_page_generator.js')
-rw-r--r-- | chrome/common/extensions/docs/js/api_page_generator.js | 510 |
1 files changed, 510 insertions, 0 deletions
diff --git a/chrome/common/extensions/docs/js/api_page_generator.js b/chrome/common/extensions/docs/js/api_page_generator.js new file mode 100644 index 0000000..856d11a --- /dev/null +++ b/chrome/common/extensions/docs/js/api_page_generator.js @@ -0,0 +1,510 @@ +/** + * @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 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 <body>. + 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" <div>. + 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. + fetchContent(SCHEMA, function(schemaContent) { + schema = JSON.parse(schemaContent); + if (pageName.toLowerCase() == "samples") { + fetchSamples(); + } else { + renderTemplate(); + } + }, function(error) { + alert("Failed to load " + SCHEMA); + }); +} + +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 = "chrome." + 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.querySelector('script[type="text/prerenderjs"]'); + if (preRender) { + preRender.parentElement.removeChild(preRender); + eval(preRender.innerText); + } + + // Render to template + var input = new JsEvalContext(pageData); + var output = document.getElementsByTagName("body")[0]; + jstProcess(input, output); + + selectCurrentPageOnLeftNav(); + + 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 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 = "<a href='" + page + "#type-" + type + "'>" + type + + "</a>"; + 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 "chrome." + module.namespace; +} + +function getFullyQualifiedFunctionName(func) { + return getModuleName() + "." + func.name; +} + +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 shouldExpandObject(object) { + return (object.type == "object" && object.properties); +} + +function getPropertyListFromObject(object) { + var propertyList = []; + for (var p in object.properties) { + var prop = object.properties[p]; + 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) { + var retval = []; + parameters.forEach(function(param, i) { + retval.push(getTypeName(param) + " " + param.name); + }); + + return retval.join(", "); +} + +function sortByName(a, b) { + if (a.name < b.name) { + return -1; + } + if (a.name > b.name) { + return 1; + } + return 0; +} |