Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / chrome / common / extensions / docs / server2 / jsc_view.py
blob2bf46d709ab56e10792650061310f7ed429ca0c2
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.
5 from copy import copy
6 import logging
7 import posixpath
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):
23 if request is None:
24 return []
25 accept_language = request.headers.get('Accept-Language', None)
26 if accept_language is None:
27 return []
28 return [lang_with_q.split(';')[0].strip()
29 for lang_with_q in accept_language.split(',')]
31 return_list = []
32 for dict_ in samples_list:
33 name = dict_['name']
34 description = dict_['description']
35 if description is None:
36 description = ''
37 if name.startswith('__MSG_') or description.startswith('__MSG_'):
38 try:
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']:
46 locale = lang
47 break
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'])
52 except Exception:
53 logging.error(traceback.format_exc())
54 # Revert the sample to the original dict.
55 sample_data = dict_
56 return_list.append(sample_data)
57 else:
58 dict_['id'] = get_sample_id(name)
59 return_list.append(dict_)
60 return return_list
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
66 member's definition.
67 '''
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'].
83 '''
84 by_name = {}
85 for item_type in GetNodeCategories():
86 if item_type in namespace:
87 old_size = len(by_name)
88 by_name.update(
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)
92 return by_name
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.
104 s = str(value)
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.
113 def __init__(self,
114 content_script_apis,
115 jsc_model,
116 availability_finder,
117 json_cache,
118 template_cache,
119 features_bundle,
120 event_byname_future,
121 platform,
122 samples):
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
147 as_dict = {
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',
156 chrome_dot_name),
157 'properties': self._GenerateProperties(self._jsc_model.properties),
158 'samples': CreateSamplesView(self._samples, request),
159 'title': self._jsc_model.documentation_options.get('title',
160 chrome_dot_name),
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)
168 return as_dict
170 def _IsExperimental(self):
171 return self._jsc_model.name.startswith('experimental')
173 def _GetChannelWarning(self):
174 if not self._IsExperimental():
175 return {
176 self._availability.channel_info.channel: True
178 return None
180 def _GenerateCallback(self, callback):
181 '''Returns a dictionary representation of a callback suitable
182 for consumption by templates.
184 if not callback:
185 return None
186 callback_dict = {
187 'name': callback.simple_name,
188 'simple_type': {'simple_type': 'function'},
189 'optional': callback.optional,
190 'parameters': []
192 with self._current_node.Descend('parameters',
193 callback.simple_name,
194 'parameters'):
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
199 return callback_dict
201 def _GenerateCallbackProperty(self, callback, callback_dict):
202 '''Returns a dictionary representation of a callback property
203 suitable for consumption by templates.
205 property_dict = {
206 'name': callback.simple_name,
207 'description': callback.description,
208 'optional': callback.optional,
209 'isCallback': True,
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
217 return property_dict
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):
229 type_dict = {
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(
237 is_enum=type_.property_type == model.PropertyType.ENUM)
239 self._RenderTypeInformation(type_, type_dict)
240 return type_dict
242 def _GenerateFunctions(self, functions):
243 '''Returns a list of dictionaries representing this Model's functions.
245 with self._current_node.Descend('functions'):
246 return [self._GenerateFunction(f) for f in functions.values()]
248 def _GenerateFunction(self, function):
249 '''Returns a dictionary representation of a function from
250 JSON Schema Compiler.
252 # When ignoring types, properties must be ignored as well.
253 with self._current_node.Descend(function.simple_name,
254 ignore=('types', 'properties')):
255 function_dict = {
256 'name': function.simple_name,
257 'description': function.description,
258 'callback': self._GenerateCallback(function.callback),
259 'parameters': [],
260 'returns': None,
261 'id': _CreateId(function, 'method'),
262 'availability': self._GetAvailabilityTemplate()
264 self._AddCommonProperties(function_dict, function)
265 if function.returns:
266 function_dict['returns'] = self._GenerateType(function.returns)
268 with self._current_node.Descend(function.simple_name, 'parameters'):
269 for param in function.params:
270 function_dict['parameters'].append(self._GenerateProperty(param))
271 if function.callback is not None:
272 # Show the callback as an extra parameter.
273 function_dict['parameters'].append(
274 self._GenerateCallbackProperty(function.callback,
275 function_dict['callback']))
276 if len(function_dict['parameters']) > 0:
277 function_dict['parameters'][-1]['last'] = True
278 return function_dict
280 def _GenerateEvents(self, events):
281 '''Returns a list of dictionaries representing this Model's events.
283 with self._current_node.Descend('events'):
284 return [self._GenerateEvent(e) for e in events.values()
285 if not e.supports_dom]
287 def _GenerateDomEvents(self, events):
288 '''Returns a list of dictionaries representing this Model's DOM events.
290 with self._current_node.Descend('events'):
291 return [self._GenerateEvent(e) for e in events.values()
292 if e.supports_dom]
294 def _GenerateEvent(self, event):
295 '''Returns a dictionary representation of an event from
296 JSON Schema Compiler. Note that although events are modeled as functions
297 in JSON Schema Compiler, we model them differently for the templates.
299 with self._current_node.Descend(event.simple_name, ignore=('properties',)):
300 event_dict = {
301 'name': event.simple_name,
302 'description': event.description,
303 'filters': [self._GenerateProperty(f) for f in event.filters],
304 'conditions': [self._GetLink(condition)
305 for condition in event.conditions],
306 'actions': [self._GetLink(action) for action in event.actions],
307 'supportsRules': event.supports_rules,
308 'supportsListeners': event.supports_listeners,
309 'properties': [],
310 'id': _CreateId(event, 'event'),
311 'byName': {},
312 'availability': self._GetAvailabilityTemplate()
314 self._AddCommonProperties(event_dict, event)
315 # Add the Event members to each event in this object.
316 if self._event_byname_future:
317 event_dict['byName'].update(self._event_byname_future.Get())
318 # We need to create the method description for addListener based on the
319 # information stored in |event|.
320 if event.supports_listeners:
321 callback_object = model.Function(parent=event,
322 name='callback',
323 json={},
324 namespace=event.parent,
325 origin='')
326 callback_object.params = event.params
327 if event.callback:
328 callback_object.callback = event.callback
330 with self._current_node.Descend(event.simple_name):
331 callback = self._GenerateFunction(callback_object)
332 callback_parameter = self._GenerateCallbackProperty(callback_object,
333 callback)
334 callback_parameter['last'] = True
335 event_dict['byName']['addListener'] = {
336 'name': 'addListener',
337 'callback': callback,
338 'parameters': [callback_parameter]
340 if event.supports_dom:
341 # Treat params as properties of the custom Event object associated with
342 # this DOM Event.
343 with self._current_node.Descend(event.simple_name,
344 ignore=('properties',)):
345 event_dict['properties'] += [self._GenerateProperty(param)
346 for param in event.params]
347 return event_dict
349 def _GenerateProperties(self, properties):
350 '''Returns a list of dictionaries representing this Model's properites.
352 with self._current_node.Descend('properties'):
353 return [self._GenerateProperty(v) for v in properties.values()]
355 def _GenerateProperty(self, property_):
356 '''Returns a dictionary representation of a property from
357 JSON Schema Compiler.
359 if not hasattr(property_, 'type_'):
360 for d in dir(property_):
361 if not d.startswith('_'):
362 print ('%s -> %s' % (d, getattr(property_, d)))
363 type_ = property_.type_
365 # Make sure we generate property info for arrays, too.
366 # TODO(kalman): what about choices?
367 if type_.property_type == model.PropertyType.ARRAY:
368 properties = type_.item_type.properties
369 else:
370 properties = type_.properties
372 with self._current_node.Descend(property_.simple_name):
373 property_dict = {
374 'name': property_.simple_name,
375 'optional': property_.optional,
376 'description': property_.description,
377 'properties': self._GenerateProperties(type_.properties),
378 'functions': self._GenerateFunctions(type_.functions),
379 'parameters': [],
380 'returns': None,
381 'id': _CreateId(property_, 'property'),
382 'availability': self._GetAvailabilityTemplate()
384 self._AddCommonProperties(property_dict, property_)
386 if type_.property_type == model.PropertyType.FUNCTION:
387 function = type_.function
388 with self._current_node.Descend('parameters'):
389 for param in function.params:
390 property_dict['parameters'].append(self._GenerateProperty(param))
391 if function.returns:
392 with self._current_node.Descend(ignore=('types', 'properties')):
393 property_dict['returns'] = self._GenerateType(function.returns)
395 value = property_.value
396 if value is not None:
397 if isinstance(value, int):
398 property_dict['value'] = _FormatValue(value)
399 else:
400 property_dict['value'] = value
401 else:
402 self._RenderTypeInformation(type_, property_dict)
404 return property_dict
406 def _AddCommonProperties(self, target, src):
407 if src.deprecated is not None:
408 target['deprecated'] = src.deprecated
409 if (src.parent is not None and
410 not isinstance(src.parent, model.Namespace)):
411 target['parentName'] = src.parent.simple_name
413 def _RenderTypeInformation(self, type_, dst_dict):
414 with self._current_node.Descend(ignore=('types', 'properties')):
415 dst_dict['is_object'] = type_.property_type == model.PropertyType.OBJECT
416 if type_.property_type == model.PropertyType.CHOICES:
417 dst_dict['choices'] = self._GenerateTypes(type_.choices)
418 # We keep track of which == last for knowing when to add "or" between
419 # choices in templates.
420 if len(dst_dict['choices']) > 0:
421 dst_dict['choices'][-1]['last'] = True
422 elif type_.property_type == model.PropertyType.REF:
423 dst_dict['link'] = self._GetLink(type_.ref_type)
424 elif type_.property_type == model.PropertyType.ARRAY:
425 dst_dict['array'] = self._GenerateType(type_.item_type)
426 elif type_.property_type == model.PropertyType.ENUM:
427 dst_dict['enum_values'] = [
428 {'name': value.name, 'description': value.description}
429 for value in type_.enum_values]
430 if len(dst_dict['enum_values']) > 0:
431 dst_dict['enum_values'][-1]['last'] = True
432 elif type_.instance_of is not None:
433 dst_dict['simple_type'] = type_.instance_of
434 else:
435 dst_dict['simple_type'] = type_.property_type.name
437 def _CreateAvailabilityTemplate(self, status, scheduled, version):
438 '''Returns an object suitable for use in templates to display availability
439 information.
441 return {
442 'partial': self._template_cache.GetFromFile(
443 '%sintro_tables/%s_message.html' % (PRIVATE_TEMPLATES, status)).Get(),
444 'scheduled': scheduled,
445 'version': version
448 def _GetAvailabilityTemplate(self, is_enum=False):
449 '''Gets availability for the current node and returns an appropriate
450 template object.
452 # We don't show an availability warning for enums.
453 # TODO(devlin): We should also render enums differently, indicating that
454 # symbolic constants are available from version 44 onwards.
455 if is_enum:
456 return None
458 # Displaying deprecated status takes precedence over when the API
459 # became stable.
460 availability_info = self._current_node.GetDeprecated()
461 if availability_info is not None:
462 status = 'deprecated'
463 else:
464 availability_info = self._current_node.GetAvailability()
465 if availability_info is None:
466 return None
467 status = availability_info.channel_info.channel
468 return self._CreateAvailabilityTemplate(
469 status,
470 availability_info.scheduled,
471 availability_info.channel_info.version)
473 def _GetIntroTableList(self):
474 '''Create a generic data structure that can be traversed by the templates
475 to create an API intro table.
477 intro_rows = [
478 self._GetIntroDescriptionRow(),
479 self._GetIntroAvailabilityRow()
480 ] + self._GetIntroDependencyRows() + self._GetIntroContentScriptRow()
482 # Add rows using data from intro_tables.json, overriding any existing rows
483 # if they share the same 'title' attribute.
484 row_titles = [row['title'] for row in intro_rows]
485 for misc_row in self._GetMiscIntroRows():
486 if misc_row['title'] in row_titles:
487 intro_rows[row_titles.index(misc_row['title'])] = misc_row
488 else:
489 intro_rows.append(misc_row)
491 return intro_rows
493 def _GetIntroContentScriptRow(self):
494 '''Generates the 'Content Script' row data for an API intro table.
496 content_script_support = self._content_script_apis.get(self._jsc_model.name)
497 if content_script_support is None:
498 return []
499 if content_script_support.restrictedTo:
500 content_script_support.restrictedTo.sort(key=itemgetter('node'))
501 MarkFirstAndLast(content_script_support.restrictedTo)
502 return [{
503 'title': 'Content Scripts',
504 'content': [{
505 'partial': self._template_cache.GetFromFile(
506 posixpath.join(PRIVATE_TEMPLATES,
507 'intro_tables',
508 'content_scripts.html')).Get(),
509 'contentScriptSupport': content_script_support.__dict__
513 def _GetIntroDescriptionRow(self):
514 ''' Generates the 'Description' row data for an API intro table.
516 return {
517 'title': 'Description',
518 'content': [
519 { 'text': self._jsc_model.description }
523 def _GetIntroAvailabilityRow(self):
524 ''' Generates the 'Availability' row data for an API intro table.
526 if self._IsExperimental():
527 status = 'experimental'
528 scheduled = None
529 version = None
530 else:
531 status = self._availability.channel_info.channel
532 scheduled = self._availability.scheduled
533 version = self._availability.channel_info.version
534 return {
535 'title': 'Availability',
536 'content': [
537 self._CreateAvailabilityTemplate(status, scheduled, version)
541 def _GetIntroDependencyRows(self):
542 # Devtools aren't in _api_features. If we're dealing with devtools, bail.
543 if 'devtools' in self._jsc_model.name:
544 return []
546 api_feature = self._api_features.Get().get(self._jsc_model.name)
547 if not api_feature:
548 logging.error('"%s" not found in _api_features.json' %
549 self._jsc_model.name)
550 return []
552 permissions_content = []
553 manifest_content = []
555 def categorize_dependency(dependency):
556 def make_code_node(text):
557 return { 'class': 'code', 'text': text }
559 context, name = dependency.split(':', 1)
560 if context == 'permission':
561 permissions_content.append(make_code_node('"%s"' % name))
562 elif context == 'manifest':
563 manifest_content.append(make_code_node('"%s": {...}' % name))
564 elif context == 'api':
565 transitive_dependencies = (
566 self._api_features.Get().get(name, {}).get('dependencies', []))
567 for transitive_dependency in transitive_dependencies:
568 categorize_dependency(transitive_dependency)
569 else:
570 logging.error('Unrecognized dependency for %s: %s' %
571 (self._jsc_model.name, context))
573 for dependency in api_feature.get('dependencies', ()):
574 categorize_dependency(dependency)
576 dependency_rows = []
577 if permissions_content:
578 dependency_rows.append({
579 'title': 'Permissions',
580 'content': permissions_content
582 if manifest_content:
583 dependency_rows.append({
584 'title': 'Manifest',
585 'content': manifest_content
587 return dependency_rows
589 def _GetMiscIntroRows(self):
590 ''' Generates miscellaneous intro table row data, such as 'Permissions',
591 'Samples', and 'Learn More', using intro_tables.json.
593 misc_rows = []
594 # Look up the API name in intro_tables.json, which is structured
595 # similarly to the data structure being created. If the name is found, loop
596 # through the attributes and add them to this structure.
597 table_info = self._intro_tables.Get().get(self._jsc_model.name)
598 if table_info is None:
599 return misc_rows
601 for category in table_info.iterkeys():
602 content = []
603 for node in table_info[category]:
604 ext_type = PlatformToExtensionType(self._platform)
605 # Don't display nodes restricted to a different platform.
606 if ext_type not in node.get('extension_types', (ext_type,)):
607 continue
608 # If there is a 'partial' argument and it hasn't already been
609 # converted to a Motemplate object, transform it to a template.
610 if 'partial' in node:
611 # Note: it's enough to copy() not deepcopy() because only a single
612 # top-level key is being modified.
613 node = copy(node)
614 node['partial'] = self._template_cache.GetFromFile(
615 posixpath.join(PRIVATE_TEMPLATES, node['partial'])).Get()
616 content.append(node)
617 misc_rows.append({ 'title': category, 'content': content })
618 return misc_rows
620 def CreateJSCView(content_script_apis,
621 jsc_model,
622 availability_finder,
623 json_cache,
624 template_cache,
625 features_bundle,
626 event_byname_future,
627 platform,
628 samples,
629 request):
630 return _JSCViewBuilder(content_script_apis,
631 jsc_model,
632 availability_finder,
633 json_cache,
634 template_cache,
635 features_bundle,
636 event_byname_future,
637 platform,
638 samples).ToDict(request)