Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(860)

Side by Side Diff: chrome/common/extensions/docs/build/directory.py

Issue 2957009: Change the existing extension samples page to an automatically-generated searchable directory. (Closed) Base URL: http://src.chromium.org/git/chromium.git
Patch Set: Make changes suggested by Antony Created 10 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 #!/usr/bin/python
2 # Copyright (c) 2010 The Chromium Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file.
5
6 """Class for parsing metadata about extension samples."""
7
8 import os
9 import os.path
10 import re
11 import hashlib
12 import simplejson as json
13
14 def parse_json_file(path, encoding="utf-8"):
15 """ Load the specified file and parse it as JSON.
16
17 Args:
18 path: Path to a file containing JSON-encoded data.
19 encoding: Encoding used in the file. Defaults to utf-8.
20
21 Returns:
22 A Python object representing the data encoded in the file.
23
24 Raises:
25 Exception: If the file could not be read or its contents could not be
26 parsed as JSON data.
27 """
28 try:
29 json_file = open(path, 'r')
30 except IOError, msg:
31 raise Exception("Failed to read the file at %s: %s" % (path, msg))
32
33 try:
34 json_obj = json.load(json_file, encoding)
35 except ValueError, msg:
36 raise Exception("Failed to parse JSON out of file %s: %s" % (path, msg))
37
38 json_file.close()
39 return json_obj
40
41 class ApiManifest(object):
42 """ Represents the list of API methods contained in extension_api.json """
43
44 _MODULE_DOC_KEYS = ['functions', 'events']
45 """ Keys which may be passed to the _parseModuleDocLinksByKey method."""
46
47 def __init__(self, manifest_path):
48 """ Read the supplied manifest file and parse its contents.
49
50 Args:
51 manifest_path: Path to extension_api.json
52 """
53 self._manifest = parse_json_file(manifest_path)
54
55 def _getDocLink(self, method, hashprefix):
56 """
57 Given an API method, return a partial URL corresponding to the doc
58 file for that method.
59
60 Args:
61 method: A string like 'chrome.foo.bar' or 'chrome.experimental.foo.onBar'
62 hashprefix: The prefix to put in front of hash links - 'method' for
63 methods and 'event' for events.
64
65 Returns:
66 A string like 'foo.html#method-bar' or 'experimental.foo.html#event-onBar'
67 """
68 urlpattern = '%%s.html#%s-%%s' % hashprefix
69 urlparts = tuple(method.replace('chrome.', '').rsplit('.', 1))
70 return urlpattern % urlparts
71
72 def _parseModuleDocLinksByKey(self, module, key):
73 """
74 Given a specific API module, returns a dict of methods or events mapped to
75 documentation URLs.
76
77 Args:
78 module: The data in extension_api.json corresponding to a single module.
79 key: A key belonging to _MODULE_DOC_KEYS to determine which set of
80 methods to parse, and what kind of documentation URL to generate.
81
82 Returns:
83 A dict of extension methods mapped to file and hash URL parts for the
84 corresponding documentation links, like:
85 {
86 "chrome.tabs.remove": "tabs.html#method-remove",
87 "chrome.tabs.onDetached" : "tabs.html#event-onDetatched"
88 }
89
90 If the API namespace is defined "nodoc" then an empty dict is returned.
91
92 Raises:
93 Exception: If the key supplied is not a member of _MODULE_DOC_KEYS.
94 """
95 methods = []
96 api_dict = {}
97 namespace = module['namespace']
98 if module.has_key('nodoc'):
99 return api_dict
100 if key not in self._MODULE_DOC_KEYS:
101 raise Exception("key %s must be one of %s" % (key, self._MODULE_DOC_KEYS))
102 if module.has_key(key):
103 methods.extend(module[key])
104 for method in methods:
105 method_name = 'chrome.%s.%s' % (namespace, method['name'])
106 hashprefix = 'method'
107 if key == 'events':
108 hashprefix = 'event'
109 api_dict[method_name] = self._getDocLink(method_name, hashprefix)
110 return api_dict
111
112 def getModuleNames(self):
113 """ Returns the names of individual modules in the API.
114
115 Returns:
116 The namespace """
117 # Exclude modules with a "nodoc" property.
118 return set(module['namespace'].encode() for module in self._manifest
119 if "nodoc" not in module)
120
121 def getDocumentationLinks(self):
122 """ Parses the extension_api.json manifest and returns a dict of all
123 events and methods for every module, mapped to relative documentation links.
124
125 Returns:
126 A dict of methods/events => partial doc links for every module.
127 """
128 api_dict = {}
129 for module in self._manifest:
130 api_dict.update(self._parseModuleDocLinksByKey(module, 'functions'))
131 api_dict.update(self._parseModuleDocLinksByKey(module, 'events'))
132 return api_dict
133
134 class SamplesManifest(object):
135 """ Represents a manifest file containing information about the sample
136 extensions available in the codebase. """
137
138 def __init__(self, base_sample_path, base_dir, api_manifest):
139 """ Reads through the filesystem and obtains information about any Chrome
140 extensions which exist underneath the specified folder.
141
142 Args:
143 base_sample_path: The directory under which to search for samples.
144 base_dir: The base directory samples will be referenced from.
145 api_manifest: An instance of the ApiManifest class, which will indicate
146 which API methods are available.
147 """
148 self._base_dir = base_dir
149 manifest_paths = self._locateManifestsFromPath(base_sample_path)
150 self._manifest_data = self._parseManifestData(manifest_paths, api_manifest)
151
152 def _locateManifestsFromPath(self, path):
153 """
154 Returns a list of paths to sample extension manifest.json files.
155
156 Args:
157 base_path: Base path in which to start the search.
158 Returns:
159 A list of paths below base_path pointing at manifest.json files.
160 """
161 manifest_paths = []
162 for root, directories, files in os.walk(path):
163 if 'manifest.json' in files:
164 directories = [] # Don't go any further down this tree
165 manifest_paths.append(os.path.join(root, 'manifest.json'))
166 if '.svn' in directories:
167 directories.remove('.svn') # Don't go into SVN metadata directories
168 return manifest_paths
169
170 def _parseManifestData(self, manifest_paths, api_manifest):
171 """ Returns metadata about the sample extensions given their manifest
172 paths.
173
174 Args:
175 manifest_paths: A list of paths to extension manifests
176 api_manifest: An instance of the ApiManifest class, which will indicate
177 which API methods are available.
178
179 Returns:
180 Manifest data containing a list of samples and available API methods.
181 """
182 api_method_dict = api_manifest.getDocumentationLinks()
183 api_methods = api_method_dict.keys()
184
185 samples = []
186 for path in manifest_paths:
187 sample = Sample(path, api_methods, self._base_dir)
188 # Don't render apps
189 if sample.is_app() == False:
190 samples.append(sample)
191 samples.sort(lambda x,y: cmp(x['name'].upper(), y['name'].upper()))
192
193 manifest_data = {'samples': samples, 'api': api_method_dict}
194 return manifest_data
195
196 def writeToFile(self, path):
197 """ Writes the contents of this manifest file as a JSON-encoded text file.
198
199 Args:
200 path: The path to write the samples manifest file to.
201 """
202 manifest_text = json.dumps(self._manifest_data, indent=2)
203 output_path = os.path.realpath(path)
204 try:
205 output_file = open(output_path, 'w')
206 except IOError, msg:
207 raise Exception("Failed to write the samples manifest file."
208 "The specific error was: %s." % msg)
209 output_file.write(manifest_text)
210 output_file.close()
211
212 class Sample(dict):
213 """ Represents metadata about a Chrome extension sample.
214
215 Extends dict so that it can be easily JSON serialized.
216 """
217
218 def __init__(self, manifest_path, api_methods, base_dir):
219 """ Initializes a Sample instance given a path to a manifest.
220
221 Args:
222 manifest_path: A filesystem path to a manifest file.
223 api_methods: A list of strings containing all possible Chrome extension
224 API calls.
225 base_dir: The base directory where this sample will be referenced from -
226 paths will be made relative to this directory.
227 """
228 self._base_dir = base_dir
229 self._manifest_path = manifest_path
230 self._manifest = parse_json_file(self._manifest_path)
231 self._locale_data = self._parse_locale_data()
232
233 # The following properties will be serialized when converting this object
234 # to JSON.
235
236 self['id'] = hashlib.sha1(manifest_path).hexdigest()
237 self['api_calls'] = self._parse_api_calls(api_methods)
238 self['name'] = self._parse_name()
239 self['description'] = self._parse_description()
240 self['icon'] = self._parse_icon()
241 self['features'] = self._parse_features()
242 self['protocols'] = self._parse_protocols()
243 self['path'] = self._get_relative_path()
244 self['search_string'] = self._get_search_string()
245 self['source_files'] = self._parse_source_files()
246
247 _FEATURE_ATTRIBUTES = (
248 'browser_action',
249 'page_action',
250 'background_page',
251 'options_page',
252 'plugins',
253 'theme',
254 'chrome_url_overrides'
255 )
256 """ Attributes that will map to "features" if their corresponding key is
257 present in the extension manifest. """
258
259 _SOURCE_FILE_EXTENSIONS = ('.html', '.json', '.js', '.css', '.htm')
260 """ File extensions to files which may contain source code."""
261
262 _ENGLISH_LOCALES = ['en_US', 'en', 'en_GB']
263 """ Locales from which translations may be used in the sample gallery. """
264
265 def _get_localized_manifest_value(self, key):
266 """ Returns a localized version of the requested manifest value.
267
268 Args:
269 key: The manifest key whose value the caller wants translated.
270
271 Returns:
272 If the supplied value exists and contains a ___MSG_token___ value, this
273 method will resolve the appropriate translation and return the result.
274 If no token exists, the manifest value will be returned. If the key does
275 not exist, an empty string will be returned.
276
277 Raises:
278 Exception: If the localized value for the given token could not be found.
279 """
280 if self._manifest.has_key(key):
281 if self._manifest[key][:6] == '__MSG_':
282 try:
283 return self._get_localized_value(self._manifest[key])
284 except Exception, msg:
285 raise Exception("Could not translate manifest value for key %s: %s" %
286 (key, msg))
287 else:
288 return self._manifest[key]
289 else:
290 return ''
291
292 def _get_localized_value(self, message_token):
293 """ Returns the localized version of the requested MSG bundle token.
294
295 Args:
296 message_token: A message bundle token like __MSG_extensionName__.
297
298 Returns:
299 The translated text corresponding to the token, with any placeholders
300 automatically resolved and substituted in.
301
302 Raises:
303 Exception: If a message bundle token is not found in the translations.
304 """
305 placeholder_pattern = re.compile('\$(\w*)\$')
306 token = message_token[6:-2]
307 if self._locale_data.has_key(token):
308 message = self._locale_data[token]['message']
309
310 placeholder_match = placeholder_pattern.search(message)
311 if placeholder_match:
312 # There are placeholders in the translation - substitute them.
313 placeholder_name = placeholder_match.group(1)
314 placeholders = self._locale_data[token]['placeholders']
315 if placeholders.has_key(placeholder_name.lower()):
316 placeholder_value = placeholders[placeholder_name.lower()]['content']
317 placeholder_token = '$%s$' % placeholder_name
318 message = message.replace(placeholder_token, placeholder_value)
319 return message
320 else:
321 raise Exception('Could not find localized string: %s' % message_token)
322
323 def _get_relative_path(self):
324 """ Returns a relative path from the supplied base dir to the manifest dir.
325
326 This method is used because we may not be able to rely on os.path.relpath
327 which was introduced in Python 2.6 and only works on Windows and Unix.
328
329 Since the example extensions should always be subdirectories of the
330 base sample manifest path, we can get a relative path through a simple
331 string substitution.
332
333 Returns:
334 A relative directory path from the sample manifest's directory to the
335 directory containing this sample's manifest.json.
336 """
337 real_manifest_path = os.path.realpath(self._manifest_path)
338 real_base_path = os.path.realpath(self._base_dir)
339 return real_manifest_path.replace(real_base_path, '')\
340 .replace('manifest.json', '')[1:]
341
342 def _get_search_string(self):
343 """ Constructs a string to be used when searching the samples list.
344
345 To make the implementation of the JavaScript-based search very direct, a
346 string is constructed containing the title, description, API calls, and
347 features that this sample uses, and is converted to uppercase. This makes
348 JavaScript sample searching very fast and easy to implement.
349
350 Returns:
351 An uppercase string containing information to match on for searching
352 samples on the client.
353 """
354 search_terms = [
355 self['name'],
356 self['description'],
357 ]
358 search_terms.extend(self['features'])
359 search_terms.extend(self['api_calls'])
360 search_string = ' '.join(search_terms).replace('"', '')\
361 .replace('\'', '')\
362 .upper()
363 return search_string
364
365 def _parse_api_calls(self, api_methods):
366 """ Returns a list of Chrome extension API calls the sample makes.
367
368 Parses any *.html and *.js files in the sample directory and matches them
369 against the supplied list of all available API methods, returning methods
370 which show up in the sample code.
371
372 Args:
373 api_methods: A list of strings containing the potential
374 API calls the and the extension sample could be making.
375
376 Returns:
377 A set of every member of api_methods that appears in any *.html or *.js
378 file contained in this sample's directory (or subdirectories).
379
380 Raises:
381 Exception: If any of the *.html or *.js files cannot be read.
382 """
383 api_calls = set()
384 extension_dir_path = os.path.dirname(self._manifest_path)
385 for root, dirs, files in os.walk(extension_dir_path):
386 for file in files:
387 if file[-5:] == '.html' or file[-3:] == '.js':
388 path = os.path.join(root, file)
389 try:
390 code_file = open(path)
391 except IOError, msg:
392 raise Exception("Failed to read %s: %s" % (path, msg))
393 code_contents = code_file.read()
394 code_file.close()
395
396 for method in api_methods:
397 if (code_contents.find(method) > -1):
398 api_calls.add(method)
399 return sorted(api_calls)
400
401 def _parse_source_files(self):
402 """ Returns a list of paths to source files present in the extenion.
403
404 Returns:
405 A list of paths relative to the manifest file directory.
406 """
407 source_paths = []
408 base_path = os.path.realpath(os.path.dirname(self._manifest_path))
409 for root, directories, files in os.walk(base_path):
410 if '.svn' in directories:
411 directories.remove('.svn') # Don't go into SVN metadata directories
412
413 for file_name in files:
414 ext = os.path.splitext(file_name)[1]
415 if ext in self._SOURCE_FILE_EXTENSIONS:
416 path = os.path.realpath(os.path.join(root, file_name))
417 path = path.replace(base_path, '')[1:]
418 source_paths.append(path)
419 return source_paths
420
421 def _parse_description(self):
422 """ Returns a localized description of the extension.
423
424 Returns:
425 A localized version of the sample's description.
426 """
427 return self._get_localized_manifest_value('description')
428
429 def _parse_features(self):
430 """ Returns a list of features the sample uses.
431
432 Returns:
433 A list of features the extension uses, as determined by
434 self._FEATURE_ATTRIBUTES.
435 """
436 features = set()
437 for feature_attr in self._FEATURE_ATTRIBUTES:
438 if self._manifest.has_key(feature_attr):
439 features.add(feature_attr)
440
441 if self._uses_popup():
442 features.add('popup')
443
444 if self._manifest.has_key('permissions'):
445 for permission in self._manifest['permissions']:
446 split = permission.split('://')
447 if (len(split) == 1):
448 features.add(split[0])
449 return sorted(features)
450
451 def _parse_icon(self):
452 """ Returns the path to the 128px icon for this sample.
453
454 Returns:
455 The path to the 128px icon if defined in the manifest, None otherwise.
456 """
457 if (self._manifest.has_key('icons') and
458 self._manifest['icons'].has_key('128')):
459 return self._manifest['icons']['128']
460 else:
461 return None
462
463 def _parse_locale_data(self):
464 """ Parses this sample's locale data into a dict.
465
466 Because the sample gallery is in English, this method only looks for
467 translations as defined by self._ENGLISH_LOCALES.
468
469 Returns:
470 A dict containing the translation keys and corresponding English text
471 for this extension.
472
473 Raises:
474 Exception: If the messages file cannot be read, or if it is improperly
475 formatted JSON.
476 """
477 en_messages = {}
478 extension_dir_path = os.path.dirname(self._manifest_path)
479 for locale in self._ENGLISH_LOCALES:
480 en_messages_path = os.path.join(extension_dir_path, '_locales', locale,
481 'messages.json')
482 if (os.path.isfile(en_messages_path)):
483 break
484
485 if (os.path.isfile(en_messages_path)):
486 try:
487 en_messages_file = open(en_messages_path, 'r')
488 except IOError, msg:
489 raise Exception("Failed to read %s: %s" % (en_messages_path, msg))
490 en_messages_contents = en_messages_file.read()
491 en_messages_file.close()
492 try:
493 en_messages = json.loads(en_messages_contents)
494 except ValueError, msg:
495 raise Exception("File %s has a syntax error: %s" %
496 (en_messages_path, msg))
497 return en_messages
498
499 def _parse_name(self):
500 """ Returns a localized name for the extension.
501
502 Returns:
503 A localized version of the sample's name.
504 """
505 return self._get_localized_manifest_value('name')
506
507 def _parse_protocols(self):
508 """ Returns a list of protocols this extension requests permission for.
509
510 Returns:
511 A list of every unique protocol listed in the manifest's permssions.
512 """
513 protocols = []
514 if self._manifest.has_key('permissions'):
515 for permission in self._manifest['permissions']:
516 split = permission.split('://')
517 if (len(split) == 2) and (split[0] not in protocols):
518 protocols.append(split[0] + "://")
519 return protocols
520
521 def _uses_background(self):
522 """ Returns true if the extension defines a background page. """
523 return self._manifest.has_key('background_page')
524
525 def _uses_browser_action(self):
526 """ Returns true if the extension defines a browser action. """
527 return self._manifest.has_key('browser_action')
528
529 def _uses_content_scripts(self):
530 """ Returns true if the extension uses content scripts. """
531 return self._manifest.has_key('content_scripts')
532
533 def _uses_options(self):
534 """ Returns true if the extension defines an options page. """
535 return self._manifest.has_key('options_page')
536
537 def _uses_page_action(self):
538 """ Returns true if the extension uses a page action. """
539 return self._manifest.has_key('page_action')
540
541 def _uses_popup(self):
542 """ Returns true if the extension defines a popup on a page or browser
543 action. """
544 has_b_popup = (self._uses_browser_action() and
545 self._manifest['browser_action'].has_key('popup'))
546 has_p_popup = (self._uses_page_action() and
547 self._manifest['page_action'].has_key('popup'))
548 return has_b_popup or has_p_popup
549
550 def is_app(self):
551 """ Returns true if the extension has an 'app' section in its manifest."""
552 return self._manifest.has_key('app')
OLDNEW
« no previous file with comments | « chrome/common/extensions/docs/build/build.py ('k') | chrome/common/extensions/docs/css/samples.css » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698