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 CreateSamplesView(samples_list, request):
19 def get_sample_id(sample_name):
20 return sample_name.lower().replace(' ', '-')
22 def get_accepted_languages(request):
25 accept_language = request.headers.get('Accept-Language', None)
26 if accept_language is None:
28 return [lang_with_q.split(';')[0].strip()
29 for lang_with_q in accept_language.split(',')]
32 for dict_ in samples_list:
34 description = dict_['description']
35 if description is None:
37 if name.startswith('__MSG_') or description.startswith('__MSG_'):
39 # Copy the sample dict so we don't change the dict in the cache.
40 sample_data = dict_.copy()
41 name_key = name[len('__MSG_'):-len('__')]
42 description_key = description[len('__MSG_'):-len('__')]
43 locale = sample_data['default_locale']
44 for lang in get_accepted_languages(request):
45 if lang in sample_data['locales']:
48 locale_data = sample_data['locales'][locale]
49 sample_data['name'] = locale_data[name_key]['message']
50 sample_data['description'] = locale_data[description_key]['message']
51 sample_data['id'] = get_sample_id(sample_data['name'])
53 logging.error(traceback.format_exc())
54 # Revert the sample to the original dict.
56 return_list.append(sample_data)
58 dict_['id'] = get_sample_id(name)
59 return_list.append(dict_)
63 def GetEventByNameFromEvents(events):
64 '''Parses the dictionary |events| to find the definitions of members of the
65 type Event. Returns a dictionary mapping the name of a member to that
68 assert 'types' in events, \
69 'The dictionary |events| must contain the key "types".'
70 event_list = [t for t in events['types'] if t.get('name') == 'Event']
71 assert len(event_list) == 1, 'Exactly one type must be called "Event".'
72 return _GetByNameDict(event_list[0])
75 def _GetByNameDict(namespace):
76 '''Returns a dictionary mapping names to named items from |namespace|.
78 This lets us render specific API entities rather than the whole thing at once,
79 for example {{apis.manifestTypes.byName.ExternallyConnectable}}.
81 Includes items from namespace['types'], namespace['functions'],
82 namespace['events'], and namespace['properties'].
85 for item_type in GetNodeCategories():
86 if item_type in namespace:
87 old_size = len(by_name)
89 (item['name'], item) for item in namespace[item_type])
90 assert len(by_name) == old_size + len(namespace[item_type]), (
91 'Duplicate name in %r' % namespace)
95 def _CreateId(node, prefix):
96 if node.parent is not None and not isinstance(node.parent, model.Namespace):
97 return '-'.join([prefix, node.parent.simple_name, node.simple_name])
98 return '-'.join([prefix, node.simple_name])
101 def _FormatValue(value):
102 '''Inserts commas every three digits for integer values. It is magic.
105 return ','.join([s[max(0, i - 3):i] for i in range(len(s), 0, -3)][::-1])
108 class _JSCViewBuilder(object):
109 '''Uses a Model from the JSON Schema Compiler and generates a dict that
110 a Motemplate template can use for a data source.
123 self._content_script_apis = content_script_apis
124 self._availability = availability_finder.GetAPIAvailability(jsc_model.name)
125 self._current_node = APINodeCursor(availability_finder, jsc_model.name)
126 self._api_availabilities = json_cache.GetFromFile(
127 posixpath.join(JSON_TEMPLATES, 'api_availabilities.json'))
128 self._intro_tables = json_cache.GetFromFile(
129 posixpath.join(JSON_TEMPLATES, 'intro_tables.json'))
130 self._api_features = features_bundle.GetAPIFeatures()
131 self._template_cache = template_cache
132 self._event_byname_future = event_byname_future
133 self._jsc_model = jsc_model
134 self._platform = platform
135 self._samples = samples
137 def _GetLink(self, link):
138 ref = link if '.' in link else (self._jsc_model.name + '.' + link)
139 return { 'ref': ref, 'text': link, 'name': link }
141 def ToDict(self, request):
142 '''Returns a dictionary representation of |self._jsc_model|, which
143 is a Namespace object from JSON Schema Compiler.
145 assert self._jsc_model is not None
146 chrome_dot_name = 'chrome.%s' % self._jsc_model.name
148 'channelWarning': self._GetChannelWarning(),
149 'documentationOptions': self._jsc_model.documentation_options,
150 'domEvents': self._GenerateDomEvents(self._jsc_model.events),
151 'events': self._GenerateEvents(self._jsc_model.events),
152 'functions': self._GenerateFunctions(self._jsc_model.functions),
153 'introList': self._GetIntroTableList(),
154 'name': self._jsc_model.name,
155 'namespace': self._jsc_model.documentation_options.get('namespace',
157 'properties': self._GenerateProperties(self._jsc_model.properties),
158 'samples': CreateSamplesView(self._samples, request),
159 'title': self._jsc_model.documentation_options.get('title',
161 'types': self._GenerateTypes(self._jsc_model.types.values()),
163 if self._jsc_model.deprecated:
164 as_dict['deprecated'] = self._jsc_model.deprecated
166 as_dict['byName'] = _GetByNameDict(as_dict)
170 def _IsExperimental(self):
171 return self._jsc_model.name.startswith('experimental')
173 def _GetChannelWarning(self):
174 if not self._IsExperimental():
176 self._availability.channel_info.channel: True
180 def _GenerateCallback(self, callback):
181 '''Returns a dictionary representation of a callback suitable
182 for consumption by templates.
187 'name': callback.simple_name,
188 'simple_type': {'simple_type': 'function'},
189 'optional': callback.optional,
192 with self._current_node.Descend('parameters',
193 callback.simple_name,
195 for param in callback.params:
196 callback_dict['parameters'].append(self._GenerateProperty(param))
197 if (len(callback_dict['parameters']) > 0):
198 callback_dict['parameters'][-1]['last'] = True
201 def _GenerateCallbackProperty(self, callback, callback_dict):
202 '''Returns a dictionary representation of a callback property
203 suitable for consumption by templates.
206 'name': callback.simple_name,
207 'description': callback.description,
208 'optional': callback.optional,
210 'asFunction': callback_dict,
211 'id': _CreateId(callback, 'property'),
212 'simple_type': 'function',
214 if (callback.parent is not None and
215 not isinstance(callback.parent, model.Namespace)):
216 property_dict['parentName'] = callback.parent.simple_name
219 def _GenerateTypes(self, types):
220 '''Returns a list of dictionaries representing this Model's types.
222 with self._current_node.Descend('types'):
223 return [self._GenerateType(t) for t in types]
225 def _GenerateType(self, type_):
226 '''Returns a dictionary representation of a type from JSON Schema Compiler.
228 with self._current_node.Descend(type_.simple_name):
230 'name': type_.simple_name,
231 'description': type_.description,
232 'properties': self._GenerateProperties(type_.properties),
233 'functions': self._GenerateFunctions(type_.functions),
234 'events': self._GenerateEvents(type_.events),
235 'id': _CreateId(type_, 'type'),
236 'availability': self._GetAvailabilityTemplate()
238 self._RenderTypeInformation(type_, type_dict)
241 def _GenerateFunctions(self, functions):
242 '''Returns a list of dictionaries representing this Model's functions.
244 with self._current_node.Descend('functions'):
245 return [self._GenerateFunction(f) for f in functions.values()]
247 def _GenerateFunction(self, function):
248 '''Returns a dictionary representation of a function from
249 JSON Schema Compiler.
251 # When ignoring types, properties must be ignored as well.
252 with self._current_node.Descend(function.simple_name,
253 ignore=('types', 'properties')):
255 'name': function.simple_name,
256 'description': function.description,
257 'callback': self._GenerateCallback(function.callback),
260 'id': _CreateId(function, 'method'),
261 'availability': self._GetAvailabilityTemplate()
263 self._AddCommonProperties(function_dict, function)
265 function_dict['returns'] = self._GenerateType(function.returns)
267 with self._current_node.Descend(function.simple_name, 'parameters'):
268 for param in function.params:
269 function_dict['parameters'].append(self._GenerateProperty(param))
270 if function.callback is not None:
271 # Show the callback as an extra parameter.
272 function_dict['parameters'].append(
273 self._GenerateCallbackProperty(function.callback,
274 function_dict['callback']))
275 if len(function_dict['parameters']) > 0:
276 function_dict['parameters'][-1]['last'] = True
279 def _GenerateEvents(self, events):
280 '''Returns a list of dictionaries representing this Model's events.
282 with self._current_node.Descend('events'):
283 return [self._GenerateEvent(e) for e in events.values()
284 if not e.supports_dom]
286 def _GenerateDomEvents(self, events):
287 '''Returns a list of dictionaries representing this Model's DOM events.
289 with self._current_node.Descend('events'):
290 return [self._GenerateEvent(e) for e in events.values()
293 def _GenerateEvent(self, event):
294 '''Returns a dictionary representation of an event from
295 JSON Schema Compiler. Note that although events are modeled as functions
296 in JSON Schema Compiler, we model them differently for the templates.
298 with self._current_node.Descend(event.simple_name, ignore=('properties',)):
300 'name': event.simple_name,
301 'description': event.description,
302 'filters': [self._GenerateProperty(f) for f in event.filters],
303 'conditions': [self._GetLink(condition)
304 for condition in event.conditions],
305 'actions': [self._GetLink(action) for action in event.actions],
306 'supportsRules': event.supports_rules,
307 'supportsListeners': event.supports_listeners,
309 'id': _CreateId(event, 'event'),
311 'availability': self._GetAvailabilityTemplate()
313 self._AddCommonProperties(event_dict, event)
314 # Add the Event members to each event in this object.
315 if self._event_byname_future:
316 event_dict['byName'].update(self._event_byname_future.Get())
317 # We need to create the method description for addListener based on the
318 # information stored in |event|.
319 if event.supports_listeners:
320 callback_object = model.Function(parent=event,
323 namespace=event.parent,
325 callback_object.params = event.params
327 callback_object.callback = event.callback
329 with self._current_node.Descend(event.simple_name):
330 callback = self._GenerateFunction(callback_object)
331 callback_parameter = self._GenerateCallbackProperty(callback_object,
333 callback_parameter['last'] = True
334 event_dict['byName']['addListener'] = {
335 'name': 'addListener',
336 'callback': callback,
337 'parameters': [callback_parameter]
339 if event.supports_dom:
340 # Treat params as properties of the custom Event object associated with
342 with self._current_node.Descend(event.simple_name,
343 ignore=('properties',)):
344 event_dict['properties'] += [self._GenerateProperty(param)
345 for param in event.params]
348 def _GenerateProperties(self, properties):
349 '''Returns a list of dictionaries representing this Model's properites.
351 with self._current_node.Descend('properties'):
352 return [self._GenerateProperty(v) for v in properties.values()]
354 def _GenerateProperty(self, property_):
355 '''Returns a dictionary representation of a property from
356 JSON Schema Compiler.
358 if not hasattr(property_, 'type_'):
359 for d in dir(property_):
360 if not d.startswith('_'):
361 print ('%s -> %s' % (d, getattr(property_, d)))
362 type_ = property_.type_
364 # Make sure we generate property info for arrays, too.
365 # TODO(kalman): what about choices?
366 if type_.property_type == model.PropertyType.ARRAY:
367 properties = type_.item_type.properties
369 properties = type_.properties
371 with self._current_node.Descend(property_.simple_name):
373 'name': property_.simple_name,
374 'optional': property_.optional,
375 'description': property_.description,
376 'properties': self._GenerateProperties(type_.properties),
377 'functions': self._GenerateFunctions(type_.functions),
380 'id': _CreateId(property_, 'property'),
381 'availability': self._GetAvailabilityTemplate()
383 self._AddCommonProperties(property_dict, property_)
385 if type_.property_type == model.PropertyType.FUNCTION:
386 function = type_.function
387 with self._current_node.Descend('parameters'):
388 for param in function.params:
389 property_dict['parameters'].append(self._GenerateProperty(param))
391 with self._current_node.Descend(ignore=('types', 'properties')):
392 property_dict['returns'] = self._GenerateType(function.returns)
394 value = property_.value
395 if value is not None:
396 if isinstance(value, int):
397 property_dict['value'] = _FormatValue(value)
399 property_dict['value'] = value
401 self._RenderTypeInformation(type_, property_dict)
405 def _AddCommonProperties(self, target, src):
406 if src.deprecated is not None:
407 target['deprecated'] = src.deprecated
408 if (src.parent is not None and
409 not isinstance(src.parent, model.Namespace)):
410 target['parentName'] = src.parent.simple_name
412 def _RenderTypeInformation(self, type_, dst_dict):
413 with self._current_node.Descend(ignore=('types', 'properties')):
414 dst_dict['is_object'] = type_.property_type == model.PropertyType.OBJECT
415 if type_.property_type == model.PropertyType.CHOICES:
416 dst_dict['choices'] = self._GenerateTypes(type_.choices)
417 # We keep track of which == last for knowing when to add "or" between
418 # choices in templates.
419 if len(dst_dict['choices']) > 0:
420 dst_dict['choices'][-1]['last'] = True
421 elif type_.property_type == model.PropertyType.REF:
422 dst_dict['link'] = self._GetLink(type_.ref_type)
423 elif type_.property_type == model.PropertyType.ARRAY:
424 dst_dict['array'] = self._GenerateType(type_.item_type)
425 elif type_.property_type == model.PropertyType.ENUM:
426 dst_dict['enum_values'] = [
427 {'name': value.name, 'description': value.description}
428 for value in type_.enum_values]
429 if len(dst_dict['enum_values']) > 0:
430 dst_dict['enum_values'][-1]['last'] = True
431 elif type_.instance_of is not None:
432 dst_dict['simple_type'] = type_.instance_of
434 dst_dict['simple_type'] = type_.property_type.name
436 def _CreateAvailabilityTemplate(self, status, scheduled, version):
437 '''Returns an object suitable for use in templates to display availability
440 # TODO(rockot): Temporary hack. Remove this very soon.
441 if status == 'master':
444 'partial': self._template_cache.GetFromFile(
445 '%sintro_tables/%s_message.html' % (PRIVATE_TEMPLATES, status)).Get(),
446 'scheduled': scheduled,
450 def _GetAvailabilityTemplate(self):
451 '''Gets availability for the current node and returns an appropriate
454 # Displaying deprecated status takes precedence over when the API
456 availability_info = self._current_node.GetDeprecated()
457 if availability_info is not None:
458 status = 'deprecated'
460 availability_info = self._current_node.GetAvailability()
461 if availability_info is None:
463 status = availability_info.channel_info.channel
464 return self._CreateAvailabilityTemplate(
466 availability_info.scheduled,
467 availability_info.channel_info.version)
469 def _GetIntroTableList(self):
470 '''Create a generic data structure that can be traversed by the templates
471 to create an API intro table.
474 self._GetIntroDescriptionRow(),
475 self._GetIntroAvailabilityRow()
476 ] + self._GetIntroDependencyRows() + self._GetIntroContentScriptRow()
478 # Add rows using data from intro_tables.json, overriding any existing rows
479 # if they share the same 'title' attribute.
480 row_titles = [row['title'] for row in intro_rows]
481 for misc_row in self._GetMiscIntroRows():
482 if misc_row['title'] in row_titles:
483 intro_rows[row_titles.index(misc_row['title'])] = misc_row
485 intro_rows.append(misc_row)
489 def _GetIntroContentScriptRow(self):
490 '''Generates the 'Content Script' row data for an API intro table.
492 content_script_support = self._content_script_apis.get(self._jsc_model.name)
493 if content_script_support is None:
495 if content_script_support.restrictedTo:
496 content_script_support.restrictedTo.sort(key=itemgetter('node'))
497 MarkFirstAndLast(content_script_support.restrictedTo)
499 'title': 'Content Scripts',
501 'partial': self._template_cache.GetFromFile(
502 posixpath.join(PRIVATE_TEMPLATES,
504 'content_scripts.html')).Get(),
505 'contentScriptSupport': content_script_support.__dict__
509 def _GetIntroDescriptionRow(self):
510 ''' Generates the 'Description' row data for an API intro table.
513 'title': 'Description',
515 { 'text': self._jsc_model.description }
519 def _GetIntroAvailabilityRow(self):
520 ''' Generates the 'Availability' row data for an API intro table.
522 if self._IsExperimental():
523 status = 'experimental'
527 status = self._availability.channel_info.channel
528 scheduled = self._availability.scheduled
529 version = self._availability.channel_info.version
531 'title': 'Availability',
533 self._CreateAvailabilityTemplate(status, scheduled, version)
537 def _GetIntroDependencyRows(self):
538 # Devtools aren't in _api_features. If we're dealing with devtools, bail.
539 if 'devtools' in self._jsc_model.name:
542 api_feature = self._api_features.Get().get(self._jsc_model.name)
544 logging.error('"%s" not found in _api_features.json' %
545 self._jsc_model.name)
548 permissions_content = []
549 manifest_content = []
551 def categorize_dependency(dependency):
552 def make_code_node(text):
553 return { 'class': 'code', 'text': text }
555 context, name = dependency.split(':', 1)
556 if context == 'permission':
557 permissions_content.append(make_code_node('"%s"' % name))
558 elif context == 'manifest':
559 manifest_content.append(make_code_node('"%s": {...}' % name))
560 elif context == 'api':
561 transitive_dependencies = (
562 self._api_features.Get().get(name, {}).get('dependencies', []))
563 for transitive_dependency in transitive_dependencies:
564 categorize_dependency(transitive_dependency)
566 logging.error('Unrecognized dependency for %s: %s' %
567 (self._jsc_model.name, context))
569 for dependency in api_feature.get('dependencies', ()):
570 categorize_dependency(dependency)
573 if permissions_content:
574 dependency_rows.append({
575 'title': 'Permissions',
576 'content': permissions_content
579 dependency_rows.append({
581 'content': manifest_content
583 return dependency_rows
585 def _GetMiscIntroRows(self):
586 ''' Generates miscellaneous intro table row data, such as 'Permissions',
587 'Samples', and 'Learn More', using intro_tables.json.
590 # Look up the API name in intro_tables.json, which is structured
591 # similarly to the data structure being created. If the name is found, loop
592 # through the attributes and add them to this structure.
593 table_info = self._intro_tables.Get().get(self._jsc_model.name)
594 if table_info is None:
597 for category in table_info.iterkeys():
599 for node in table_info[category]:
600 ext_type = PlatformToExtensionType(self._platform)
601 # Don't display nodes restricted to a different platform.
602 if ext_type not in node.get('extension_types', (ext_type,)):
604 # If there is a 'partial' argument and it hasn't already been
605 # converted to a Motemplate object, transform it to a template.
606 if 'partial' in node:
607 # Note: it's enough to copy() not deepcopy() because only a single
608 # top-level key is being modified.
610 node['partial'] = self._template_cache.GetFromFile(
611 posixpath.join(PRIVATE_TEMPLATES, node['partial'])).Get()
613 misc_rows.append({ 'title': category, 'content': content })
616 def CreateJSCView(content_script_apis,
626 return _JSCViewBuilder(content_script_apis,
634 samples).ToDict(request)