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
(
237 is_enum
=type_
.property_type
== model
.PropertyType
.ENUM
)
239 self
._RenderTypeInformation
(type_
, 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')):
256 'name': function
.simple_name
,
257 'description': function
.description
,
258 'callback': self
._GenerateCallback
(function
.callback
),
261 'id': _CreateId(function
, 'method'),
262 'availability': self
._GetAvailabilityTemplate
()
264 self
._AddCommonProperties
(function_dict
, function
)
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
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()
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',)):
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
,
310 'id': _CreateId(event
, 'event'),
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
,
324 namespace
=event
.parent
,
326 callback_object
.params
= event
.params
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
,
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
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
]
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
370 properties
= type_
.properties
372 with self
._current
_node
.Descend(property_
.simple_name
):
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
),
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
))
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
)
400 property_dict
['value'] = value
402 self
._RenderTypeInformation
(type_
, 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
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
442 'partial': self
._template
_cache
.GetFromFile(
443 '%sintro_tables/%s_message.html' % (PRIVATE_TEMPLATES
, status
)).Get(),
444 'scheduled': scheduled
,
448 def _GetAvailabilityTemplate(self
, is_enum
=False):
449 '''Gets availability for the current node and returns an appropriate
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.
458 # Displaying deprecated status takes precedence over when the API
460 availability_info
= self
._current
_node
.GetDeprecated()
461 if availability_info
is not None:
462 status
= 'deprecated'
464 availability_info
= self
._current
_node
.GetAvailability()
465 if availability_info
is None:
467 status
= availability_info
.channel_info
.channel
468 return self
._CreateAvailabilityTemplate
(
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.
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
489 intro_rows
.append(misc_row
)
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:
499 if content_script_support
.restrictedTo
:
500 content_script_support
.restrictedTo
.sort(key
=itemgetter('node'))
501 MarkFirstAndLast(content_script_support
.restrictedTo
)
503 'title': 'Content Scripts',
505 'partial': self
._template
_cache
.GetFromFile(
506 posixpath
.join(PRIVATE_TEMPLATES
,
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.
517 'title': 'Description',
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'
531 status
= self
._availability
.channel_info
.channel
532 scheduled
= self
._availability
.scheduled
533 version
= self
._availability
.channel_info
.version
535 'title': 'Availability',
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
:
546 api_feature
= self
._api
_features
.Get().get(self
._jsc
_model
.name
)
548 logging
.error('"%s" not found in _api_features.json' %
549 self
._jsc
_model
.name
)
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
)
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
)
577 if permissions_content
:
578 dependency_rows
.append({
579 'title': 'Permissions',
580 'content': permissions_content
583 dependency_rows
.append({
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.
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:
601 for category
in table_info
.iterkeys():
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
,)):
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.
614 node
['partial'] = self
._template
_cache
.GetFromFile(
615 posixpath
.join(PRIVATE_TEMPLATES
, node
['partial'])).Get()
617 misc_rows
.append({ 'title': category
, 'content': content
})
620 def CreateJSCView(content_script_apis
,
630 return _JSCViewBuilder(content_script_apis
,
638 samples
).ToDict(request
)