summaryrefslogtreecommitdiffstats
path: root/chrome/common/extensions/docs/js/api_page_generator.js
blob: dbd30dc1ab41813e30a62096f4ae70dae88cffa2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
/**
 * @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 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;

// 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 title of the api module
var apiModuleTitle;

Array.prototype.each = function(f) {
  for (var i = 0; i < this.length; i++) {
    f(this[i], i);
  }
}

// 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];
  }
}

Array.prototype.map = function(f) {
  var retval = [];
  for (var i = 0; i < this.length; i++) {
    retval.push(f(this[i], i));
  }
  return retval;
}

// 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() {
  var pathParts = document.location.href.split(/\/|\./);
  pageBase = pathParts[pathParts.length - 2];
  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);
    renderTemplate();
    
  }, function(error) {
    alert("Failed to load " + SCHEMA);
  });
}

/**
 * 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.each(function(mod) {
    if (mod.namespace == pageBase) {
      // This page is an api page. Setup types and apiDefinition.
      module = mod;
      apiModuleTitle = "chrome." + module.namespace;  
      pageData.apiDefinition = module;
    }
    
    if (mod.types) {
      mod.types.each(function(type) {
        typeModule[type.id] = mod;
      });
    }
  });

  // 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.each(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", "leftNav").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 getDataFromPageHTML(id) {
  var node = document.getElementById(id);
  if (!node)
    return;
  return node.innerHTML;
}

function isArray(type) {
  return type.type == 'array';
}

function getTypeRef(type) {
  return type["$ref"];
}

function showPageTOC() {
  return module || getDataFromPageHTML('pageData-showTOC');
}

function getStaticTOC() {
  var staticHNodes = evalXPathFromId(".//h2|h3", "static");
  var retval = [];
  var lastH2;
  
  staticHNodes.each(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;
}

function getTypeRefPage(type) {
  return typeModule[type.$ref].namespace + ".html";
}

function getPageTitle() {
  return getDataFromPageHTML("pageData-title") || 
         apiModuleTitle || 
         pageName;
}

function getModuleName() {
  return "chrome." + module.namespace;
}

function getFullyQualifiedFunctionName(func) {
  return getModuleName() + "." + func.name;
}

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.each(function(c) {
      typeNames.push(getTypeName(c));
    });

    return typeNames.join(" or ");
  }

  if (schema.type == "array")
    return "array of " + getTypeName(schema.items);

  return schema.type;
}

function getSignatureString(parameters) {
  var retval = [];
  parameters.each(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;
}