1 # Copyright 2014 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.
9 from api_models import GetNodeCategories
10 from api_schema_graph import APINodeCursor
11 from docs_server_utils import MarkFirstAndLast
12 from extensions_paths import JSON_TEMPLATES, PRIVATE_TEMPLATES
13 from operator import itemgetter
14 from platform_util import PlatformToExtensionType
15 import third_party.json_schema_compiler.model as model
18 def GetEventByNameFromEvents(events):
19 '''Parses the dictionary |events| to find the definitions of members of the
20 type Event. Returns a dictionary mapping the name of a member to that
23 assert 'types' in events, \
24 'The dictionary |events| must contain the key "types".'
25 event_list = [t for t in events['types'] if t.get('name') == 'Event']
26 assert len(event_list) == 1, 'Exactly one type must be called "Event".'
27 return _GetByNameDict(event_list[0])
30 def _GetByNameDict(namespace):
31 '''Returns a dictionary mapping names to named items from |namespace|.
33 This lets us render specific API entities rather than the whole thing at once,
34 for example {{apis.manifestTypes.byName.ExternallyConnectable}}.
36 Includes items from namespace['types'], namespace['functions'],
37 namespace['events'], and namespace['properties'].
40 for item_type in GetNodeCategories():
41 if item_type in namespace:
42 old_size = len(by_name)
44 (item['name'], item) for item in namespace[item_type])
45 assert len(by_name) == old_size + len(namespace[item_type]), (
46 'Duplicate name in %r' % namespace)
50 def _CreateId(node, prefix):
51 if node.parent is not None and not isinstance(node.parent, model.Namespace):
52 return '-'.join([prefix, node.parent.simple_name, node.simple_name])
53 return '-'.join([prefix, node.simple_name])
56 def _FormatValue(value):
57 '''Inserts commas every three digits for integer values. It is magic.
60 return ','.join([s[max(0, i - 3):i] for i in range(len(s), 0, -3)][::-1])
63 class JSCView(object):
64 '''Uses a Model from the JSON Schema Compiler and generates a dict that
65 a Motemplate template can use for a data source.
77 self._content_script_apis = content_script_apis
78 self._availability = availability_finder.GetAPIAvailability(jsc_model.name)
79 self._current_node = APINodeCursor(availability_finder, jsc_model.name)
80 self._api_availabilities = json_cache.GetFromFile(
81 posixpath.join(JSON_TEMPLATES, 'api_availabilities.json'))
82 self._intro_tables = json_cache.GetFromFile(
83 posixpath.join(JSON_TEMPLATES, 'intro_tables.json'))
84 self._api_features = features_bundle.GetAPIFeatures()
85 self._template_cache = template_cache
86 self._event_byname_future = event_byname_future
87 self._jsc_model = jsc_model
88 self._platform = platform
90 def _GetLink(self, link):
91 ref = link if '.' in link else (self._jsc_model.name + '.' + link)
92 return { 'ref': ref, 'text': link, 'name': link }
95 '''Returns a dictionary representation of |self._jsc_model|, which
96 is a Namespace object from JSON Schema Compiler.
98 assert self._jsc_model is not None
99 chrome_dot_name = 'chrome.%s' % self._jsc_model.name
101 'name': self._jsc_model.name,
102 'namespace': self._jsc_model.documentation_options.get('namespace',
104 'title': self._jsc_model.documentation_options.get('title',
106 'documentationOptions': self._jsc_model.documentation_options,
107 'types': self._GenerateTypes(self._jsc_model.types.values()),
108 'functions': self._GenerateFunctions(self._jsc_model.functions),
109 'events': self._GenerateEvents(self._jsc_model.events),
110 'domEvents': self._GenerateDomEvents(self._jsc_model.events),
111 'properties': self._GenerateProperties(self._jsc_model.properties),
112 'introList': self._GetIntroTableList(),
113 'channelWarning': self._GetChannelWarning(),
115 if self._jsc_model.deprecated:
116 as_dict['deprecated'] = self._jsc_model.deprecated
118 as_dict['byName'] = _GetByNameDict(as_dict)
122 def _IsExperimental(self):
123 return self._jsc_model.name.startswith('experimental')
125 def _GetChannelWarning(self):
126 if not self._IsExperimental():
128 self._availability.channel_info.channel: True
132 def _GenerateCallback(self, callback):
133 '''Returns a dictionary representation of a callback suitable
134 for consumption by templates.
139 'name': callback.simple_name,
140 'simple_type': {'simple_type': 'function'},
141 'optional': callback.optional,
144 with self._current_node.Descend('parameters',
145 callback.simple_name,
147 for param in callback.params:
148 callback_dict['parameters'].append(self._GenerateProperty(param))
149 if (len(callback_dict['parameters']) > 0):
150 callback_dict['parameters'][-1]['last'] = True
153 def _GenerateCallbackProperty(self, callback, callback_dict):
154 '''Returns a dictionary representation of a callback property
155 suitable for consumption by templates.
158 'name': callback.simple_name,
159 'description': callback.description,
160 'optional': callback.optional,
162 'asFunction': callback_dict,
163 'id': _CreateId(callback, 'property'),
164 'simple_type': 'function',
166 if (callback.parent is not None and
167 not isinstance(callback.parent, model.Namespace)):
168 property_dict['parentName'] = callback.parent.simple_name
171 def _GenerateTypes(self, types):
172 '''Returns a list of dictionaries representing this Model's types.
174 with self._current_node.Descend('types'):
175 return [self._GenerateType(t) for t in types]
177 def _GenerateType(self, type_):
178 '''Returns a dictionary representation of a type from JSON Schema Compiler.
180 with self._current_node.Descend(type_.simple_name):
182 'name': type_.simple_name,
183 'description': type_.description,
184 'properties': self._GenerateProperties(type_.properties),
185 'functions': self._GenerateFunctions(type_.functions),
186 'events': self._GenerateEvents(type_.events),
187 'id': _CreateId(type_, 'type'),
188 'availability': self._GetAvailabilityTemplate()
190 self._RenderTypeInformation(type_, type_dict)
193 def _GenerateFunctions(self, functions):
194 '''Returns a list of dictionaries representing this Model's functions.
196 with self._current_node.Descend('functions'):
197 return [self._GenerateFunction(f) for f in functions.values()]
199 def _GenerateFunction(self, function):
200 '''Returns a dictionary representation of a function from
201 JSON Schema Compiler.
203 # When ignoring types, properties must be ignored as well.
204 with self._current_node.Descend(function.simple_name,
205 ignore=('types', 'properties')):
207 'name': function.simple_name,
208 'description': function.description,
209 'callback': self._GenerateCallback(function.callback),
212 'id': _CreateId(function, 'method'),
213 'availability': self._GetAvailabilityTemplate()
215 self._AddCommonProperties(function_dict, function)
217 function_dict['returns'] = self._GenerateType(function.returns)
219 with self._current_node.Descend(function.simple_name, 'parameters'):
220 for param in function.params:
221 function_dict['parameters'].append(self._GenerateProperty(param))
222 if function.callback is not None:
223 # Show the callback as an extra parameter.
224 function_dict['parameters'].append(
225 self._GenerateCallbackProperty(function.callback,
226 function_dict['callback']))
227 if len(function_dict['parameters']) > 0:
228 function_dict['parameters'][-1]['last'] = True
231 def _GenerateEvents(self, events):
232 '''Returns a list of dictionaries representing this Model's events.
234 with self._current_node.Descend('events'):
235 return [self._GenerateEvent(e) for e in events.values()
236 if not e.supports_dom]
238 def _GenerateDomEvents(self, events):
239 '''Returns a list of dictionaries representing this Model's DOM events.
241 with self._current_node.Descend('events'):
242 return [self._GenerateEvent(e) for e in events.values()
245 def _GenerateEvent(self, event):
246 '''Returns a dictionary representation of an event from
247 JSON Schema Compiler. Note that although events are modeled as functions
248 in JSON Schema Compiler, we model them differently for the templates.
250 with self._current_node.Descend(event.simple_name, ignore=('properties',)):
252 'name': event.simple_name,
253 'description': event.description,
254 'filters': [self._GenerateProperty(f) for f in event.filters],
255 'conditions': [self._GetLink(condition)
256 for condition in event.conditions],
257 'actions': [self._GetLink(action) for action in event.actions],
258 'supportsRules': event.supports_rules,
259 'supportsListeners': event.supports_listeners,
261 'id': _CreateId(event, 'event'),
263 'availability': self._GetAvailabilityTemplate()
265 self._AddCommonProperties(event_dict, event)
266 # Add the Event members to each event in this object.
267 if self._event_byname_future:
268 event_dict['byName'].update(self._event_byname_future.Get())
269 # We need to create the method description for addListener based on the
270 # information stored in |event|.
271 if event.supports_listeners:
272 callback_object = model.Function(parent=event,
275 namespace=event.parent,
277 callback_object.params = event.params
279 callback_object.callback = event.callback
281 with self._current_node.Descend(event.simple_name):
282 callback = self._GenerateFunction(callback_object)
283 callback_parameter = self._GenerateCallbackProperty(callback_object,
285 callback_parameter['last'] = True
286 event_dict['byName']['addListener'] = {
287 'name': 'addListener',
288 'callback': callback,
289 'parameters': [callback_parameter]
291 if event.supports_dom:
292 # Treat params as properties of the custom Event object associated with
294 with self._current_node.Descend(event.simple_name,
295 ignore=('properties',)):
296 event_dict['properties'] += [self._GenerateProperty(param)
297 for param in event.params]
300 def _GenerateProperties(self, properties):
301 '''Returns a list of dictionaries representing this Model's properites.
303 with self._current_node.Descend('properties'):
304 return [self._GenerateProperty(v) for v in properties.values()]
306 def _GenerateProperty(self, property_):
307 '''Returns a dictionary representation of a property from
308 JSON Schema Compiler.
310 if not hasattr(property_, 'type_'):
311 for d in dir(property_):
312 if not d.startswith('_'):
313 print ('%s -> %s' % (d, getattr(property_, d)))
314 type_ = property_.type_
316 # Make sure we generate property info for arrays, too.
317 # TODO(kalman): what about choices?
318 if type_.property_type == model.PropertyType.ARRAY:
319 properties = type_.item_type.properties
321 properties = type_.properties
323 with self._current_node.Descend(property_.simple_name):
325 'name': property_.simple_name,
326 'optional': property_.optional,
327 'description': property_.description,
328 'properties': self._GenerateProperties(type_.properties),
329 'functions': self._GenerateFunctions(type_.functions),
332 'id': _CreateId(property_, 'property'),
333 'availability': self._GetAvailabilityTemplate()
335 self._AddCommonProperties(property_dict, property_)
337 if type_.property_type == model.PropertyType.FUNCTION:
338 function = type_.function
339 with self._current_node.Descend('parameters'):
340 for param in function.params:
341 property_dict['parameters'].append(self._GenerateProperty(param))
343 with self._current_node.Descend(ignore=('types', 'properties')):
344 property_dict['returns'] = self._GenerateType(function.returns)
346 value = property_.value
347 if value is not None:
348 if isinstance(value, int):
349 property_dict['value'] = _FormatValue(value)
351 property_dict['value'] = value
353 self._RenderTypeInformation(type_, property_dict)
357 def _AddCommonProperties(self, target, src):
358 if src.deprecated is not None:
359 target['deprecated'] = src.deprecated
360 if (src.parent is not None and
361 not isinstance(src.parent, model.Namespace)):
362 target['parentName'] = src.parent.simple_name
364 def _RenderTypeInformation(self, type_, dst_dict):
365 with self._current_node.Descend(ignore=('types', 'properties')):
366 dst_dict['is_object'] = type_.property_type == model.PropertyType.OBJECT
367 if type_.property_type == model.PropertyType.CHOICES:
368 dst_dict['choices'] = self._GenerateTypes(type_.choices)
369 # We keep track of which == last for knowing when to add "or" between
370 # choices in templates.
371 if len(dst_dict['choices']) > 0:
372 dst_dict['choices'][-1]['last'] = True
373 elif type_.property_type == model.PropertyType.REF:
374 dst_dict['link'] = self._GetLink(type_.ref_type)
375 elif type_.property_type == model.PropertyType.ARRAY:
376 dst_dict['array'] = self._GenerateType(type_.item_type)
377 elif type_.property_type == model.PropertyType.ENUM:
378 dst_dict['enum_values'] = [
379 {'name': value.name, 'description': value.description}
380 for value in type_.enum_values]
381 if len(dst_dict['enum_values']) > 0:
382 dst_dict['enum_values'][-1]['last'] = True
383 elif type_.instance_of is not None:
384 dst_dict['simple_type'] = type_.instance_of
386 dst_dict['simple_type'] = type_.property_type.name
388 def _CreateAvailabilityTemplate(self, status, scheduled, version):
389 '''Returns an object suitable for use in templates to display availability
392 # TODO(rockot): Temporary hack. Remove this very soon.
393 if status == 'master':
396 'partial': self._template_cache.GetFromFile(
397 '%sintro_tables/%s_message.html' % (PRIVATE_TEMPLATES, status)).Get(),
398 'scheduled': scheduled,
402 def _GetAvailabilityTemplate(self):
403 '''Gets availability for the current node and returns an appropriate
406 # Displaying deprecated status takes precedence over when the API
408 availability_info = self._current_node.GetDeprecated()
409 if availability_info is not None:
410 status = 'deprecated'
412 availability_info = self._current_node.GetAvailability()
413 if availability_info is None:
415 status = availability_info.channel_info.channel
416 return self._CreateAvailabilityTemplate(
418 availability_info.scheduled,
419 availability_info.channel_info.version)
421 def _GetIntroTableList(self):
422 '''Create a generic data structure that can be traversed by the templates
423 to create an API intro table.
426 self._GetIntroDescriptionRow(),
427 self._GetIntroAvailabilityRow()
428 ] + self._GetIntroDependencyRows() + self._GetIntroContentScriptRow()
430 # Add rows using data from intro_tables.json, overriding any existing rows
431 # if they share the same 'title' attribute.
432 row_titles = [row['title'] for row in intro_rows]
433 for misc_row in self._GetMiscIntroRows():
434 if misc_row['title'] in row_titles:
435 intro_rows[row_titles.index(misc_row['title'])] = misc_row
437 intro_rows.append(misc_row)
441 def _GetIntroContentScriptRow(self):
442 '''Generates the 'Content Script' row data for an API intro table.
444 content_script_support = self._content_script_apis.get(self._jsc_model.name)
445 if content_script_support is None:
447 if content_script_support.restrictedTo:
448 content_script_support.restrictedTo.sort(key=itemgetter('node'))
449 MarkFirstAndLast(content_script_support.restrictedTo)
451 'title': 'Content Scripts',
453 'partial': self._template_cache.GetFromFile(
454 posixpath.join(PRIVATE_TEMPLATES,
456 'content_scripts.html')).Get(),
457 'contentScriptSupport': content_script_support.__dict__
461 def _GetIntroDescriptionRow(self):
462 ''' Generates the 'Description' row data for an API intro table.
465 'title': 'Description',
467 { 'text': self._jsc_model.description }
471 def _GetIntroAvailabilityRow(self):
472 ''' Generates the 'Availability' row data for an API intro table.
474 if self._IsExperimental():
475 status = 'experimental'
479 status = self._availability.channel_info.channel
480 scheduled = self._availability.scheduled
481 version = self._availability.channel_info.version
483 'title': 'Availability',
485 self._CreateAvailabilityTemplate(status, scheduled, version)
489 def _GetIntroDependencyRows(self):
490 # Devtools aren't in _api_features. If we're dealing with devtools, bail.
491 if 'devtools' in self._jsc_model.name:
494 api_feature = self._api_features.Get().get(self._jsc_model.name)
496 logging.error('"%s" not found in _api_features.json' %
497 self._jsc_model.name)
500 permissions_content = []
501 manifest_content = []
503 def categorize_dependency(dependency):
504 def make_code_node(text):
505 return { 'class': 'code', 'text': text }
507 context, name = dependency.split(':', 1)
508 if context == 'permission':
509 permissions_content.append(make_code_node('"%s"' % name))
510 elif context == 'manifest':
511 manifest_content.append(make_code_node('"%s": {...}' % name))
512 elif context == 'api':
513 transitive_dependencies = (
514 self._api_features.Get().get(name, {}).get('dependencies', []))
515 for transitive_dependency in transitive_dependencies:
516 categorize_dependency(transitive_dependency)
518 logging.error('Unrecognized dependency for %s: %s' %
519 (self._jsc_model.name, context))
521 for dependency in api_feature.get('dependencies', ()):
522 categorize_dependency(dependency)
525 if permissions_content:
526 dependency_rows.append({
527 'title': 'Permissions',
528 'content': permissions_content
531 dependency_rows.append({
533 'content': manifest_content
535 return dependency_rows
537 def _GetMiscIntroRows(self):
538 ''' Generates miscellaneous intro table row data, such as 'Permissions',
539 'Samples', and 'Learn More', using intro_tables.json.
542 # Look up the API name in intro_tables.json, which is structured
543 # similarly to the data structure being created. If the name is found, loop
544 # through the attributes and add them to this structure.
545 table_info = self._intro_tables.Get().get(self._jsc_model.name)
546 if table_info is None:
549 for category in table_info.iterkeys():
551 for node in table_info[category]:
552 ext_type = PlatformToExtensionType(self._platform)
553 # Don't display nodes restricted to a different platform.
554 if ext_type not in node.get('extension_types', (ext_type,)):
556 # If there is a 'partial' argument and it hasn't already been
557 # converted to a Motemplate object, transform it to a template.
558 if 'partial' in node:
559 # Note: it's enough to copy() not deepcopy() because only a single
560 # top-level key is being modified.
562 node['partial'] = self._template_cache.GetFromFile(
563 posixpath.join(PRIVATE_TEMPLATES, node['partial'])).Get()
565 misc_rows.append({ 'title': category, 'content': content })