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