| Index: chrome/common/extensions/docs/build/directory.py
|
| diff --git a/chrome/common/extensions/docs/build/directory.py b/chrome/common/extensions/docs/build/directory.py
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..b736263b0051d57cbe92a13db23ce5e0910b56e9
|
| --- /dev/null
|
| +++ b/chrome/common/extensions/docs/build/directory.py
|
| @@ -0,0 +1,552 @@
|
| +#!/usr/bin/python
|
| +# Copyright (c) 2010 The Chromium Authors. All rights reserved.
|
| +# Use of this source code is governed by a BSD-style license that can be
|
| +# found in the LICENSE file.
|
| +
|
| +"""Class for parsing metadata about extension samples."""
|
| +
|
| +import os
|
| +import os.path
|
| +import re
|
| +import hashlib
|
| +import simplejson as json
|
| +
|
| +def parse_json_file(path, encoding="utf-8"):
|
| + """ Load the specified file and parse it as JSON.
|
| +
|
| + Args:
|
| + path: Path to a file containing JSON-encoded data.
|
| + encoding: Encoding used in the file. Defaults to utf-8.
|
| +
|
| + Returns:
|
| + A Python object representing the data encoded in the file.
|
| +
|
| + Raises:
|
| + Exception: If the file could not be read or its contents could not be
|
| + parsed as JSON data.
|
| + """
|
| + try:
|
| + json_file = open(path, 'r')
|
| + except IOError, msg:
|
| + raise Exception("Failed to read the file at %s: %s" % (path, msg))
|
| +
|
| + try:
|
| + json_obj = json.load(json_file, encoding)
|
| + except ValueError, msg:
|
| + raise Exception("Failed to parse JSON out of file %s: %s" % (path, msg))
|
| +
|
| + json_file.close()
|
| + return json_obj
|
| +
|
| +class ApiManifest(object):
|
| + """ Represents the list of API methods contained in extension_api.json """
|
| +
|
| + _MODULE_DOC_KEYS = ['functions', 'events']
|
| + """ Keys which may be passed to the _parseModuleDocLinksByKey method."""
|
| +
|
| + def __init__(self, manifest_path):
|
| + """ Read the supplied manifest file and parse its contents.
|
| +
|
| + Args:
|
| + manifest_path: Path to extension_api.json
|
| + """
|
| + self._manifest = parse_json_file(manifest_path)
|
| +
|
| + def _getDocLink(self, method, hashprefix):
|
| + """
|
| + Given an API method, return a partial URL corresponding to the doc
|
| + file for that method.
|
| +
|
| + Args:
|
| + method: A string like 'chrome.foo.bar' or 'chrome.experimental.foo.onBar'
|
| + hashprefix: The prefix to put in front of hash links - 'method' for
|
| + methods and 'event' for events.
|
| +
|
| + Returns:
|
| + A string like 'foo.html#method-bar' or 'experimental.foo.html#event-onBar'
|
| + """
|
| + urlpattern = '%%s.html#%s-%%s' % hashprefix
|
| + urlparts = tuple(method.replace('chrome.', '').rsplit('.', 1))
|
| + return urlpattern % urlparts
|
| +
|
| + def _parseModuleDocLinksByKey(self, module, key):
|
| + """
|
| + Given a specific API module, returns a dict of methods or events mapped to
|
| + documentation URLs.
|
| +
|
| + Args:
|
| + module: The data in extension_api.json corresponding to a single module.
|
| + key: A key belonging to _MODULE_DOC_KEYS to determine which set of
|
| + methods to parse, and what kind of documentation URL to generate.
|
| +
|
| + Returns:
|
| + A dict of extension methods mapped to file and hash URL parts for the
|
| + corresponding documentation links, like:
|
| + {
|
| + "chrome.tabs.remove": "tabs.html#method-remove",
|
| + "chrome.tabs.onDetached" : "tabs.html#event-onDetatched"
|
| + }
|
| +
|
| + If the API namespace is defined "nodoc" then an empty dict is returned.
|
| +
|
| + Raises:
|
| + Exception: If the key supplied is not a member of _MODULE_DOC_KEYS.
|
| + """
|
| + methods = []
|
| + api_dict = {}
|
| + namespace = module['namespace']
|
| + if module.has_key('nodoc'):
|
| + return api_dict
|
| + if key not in self._MODULE_DOC_KEYS:
|
| + raise Exception("key %s must be one of %s" % (key, self._MODULE_DOC_KEYS))
|
| + if module.has_key(key):
|
| + methods.extend(module[key])
|
| + for method in methods:
|
| + method_name = 'chrome.%s.%s' % (namespace, method['name'])
|
| + hashprefix = 'method'
|
| + if key == 'events':
|
| + hashprefix = 'event'
|
| + api_dict[method_name] = self._getDocLink(method_name, hashprefix)
|
| + return api_dict
|
| +
|
| + def getModuleNames(self):
|
| + """ Returns the names of individual modules in the API.
|
| +
|
| + Returns:
|
| + The namespace """
|
| + # Exclude modules with a "nodoc" property.
|
| + return set(module['namespace'].encode() for module in self._manifest
|
| + if "nodoc" not in module)
|
| +
|
| + def getDocumentationLinks(self):
|
| + """ Parses the extension_api.json manifest and returns a dict of all
|
| + events and methods for every module, mapped to relative documentation links.
|
| +
|
| + Returns:
|
| + A dict of methods/events => partial doc links for every module.
|
| + """
|
| + api_dict = {}
|
| + for module in self._manifest:
|
| + api_dict.update(self._parseModuleDocLinksByKey(module, 'functions'))
|
| + api_dict.update(self._parseModuleDocLinksByKey(module, 'events'))
|
| + return api_dict
|
| +
|
| +class SamplesManifest(object):
|
| + """ Represents a manifest file containing information about the sample
|
| + extensions available in the codebase. """
|
| +
|
| + def __init__(self, base_sample_path, base_dir, api_manifest):
|
| + """ Reads through the filesystem and obtains information about any Chrome
|
| + extensions which exist underneath the specified folder.
|
| +
|
| + Args:
|
| + base_sample_path: The directory under which to search for samples.
|
| + base_dir: The base directory samples will be referenced from.
|
| + api_manifest: An instance of the ApiManifest class, which will indicate
|
| + which API methods are available.
|
| + """
|
| + self._base_dir = base_dir
|
| + manifest_paths = self._locateManifestsFromPath(base_sample_path)
|
| + self._manifest_data = self._parseManifestData(manifest_paths, api_manifest)
|
| +
|
| + def _locateManifestsFromPath(self, path):
|
| + """
|
| + Returns a list of paths to sample extension manifest.json files.
|
| +
|
| + Args:
|
| + base_path: Base path in which to start the search.
|
| + Returns:
|
| + A list of paths below base_path pointing at manifest.json files.
|
| + """
|
| + manifest_paths = []
|
| + for root, directories, files in os.walk(path):
|
| + if 'manifest.json' in files:
|
| + directories = [] # Don't go any further down this tree
|
| + manifest_paths.append(os.path.join(root, 'manifest.json'))
|
| + if '.svn' in directories:
|
| + directories.remove('.svn') # Don't go into SVN metadata directories
|
| + return manifest_paths
|
| +
|
| + def _parseManifestData(self, manifest_paths, api_manifest):
|
| + """ Returns metadata about the sample extensions given their manifest
|
| + paths.
|
| +
|
| + Args:
|
| + manifest_paths: A list of paths to extension manifests
|
| + api_manifest: An instance of the ApiManifest class, which will indicate
|
| + which API methods are available.
|
| +
|
| + Returns:
|
| + Manifest data containing a list of samples and available API methods.
|
| + """
|
| + api_method_dict = api_manifest.getDocumentationLinks()
|
| + api_methods = api_method_dict.keys()
|
| +
|
| + samples = []
|
| + for path in manifest_paths:
|
| + sample = Sample(path, api_methods, self._base_dir)
|
| + # Don't render apps
|
| + if sample.is_app() == False:
|
| + samples.append(sample)
|
| + samples.sort(lambda x,y: cmp(x['name'].upper(), y['name'].upper()))
|
| +
|
| + manifest_data = {'samples': samples, 'api': api_method_dict}
|
| + return manifest_data
|
| +
|
| + def writeToFile(self, path):
|
| + """ Writes the contents of this manifest file as a JSON-encoded text file.
|
| +
|
| + Args:
|
| + path: The path to write the samples manifest file to.
|
| + """
|
| + manifest_text = json.dumps(self._manifest_data, indent=2)
|
| + output_path = os.path.realpath(path)
|
| + try:
|
| + output_file = open(output_path, 'w')
|
| + except IOError, msg:
|
| + raise Exception("Failed to write the samples manifest file."
|
| + "The specific error was: %s." % msg)
|
| + output_file.write(manifest_text)
|
| + output_file.close()
|
| +
|
| +class Sample(dict):
|
| + """ Represents metadata about a Chrome extension sample.
|
| +
|
| + Extends dict so that it can be easily JSON serialized.
|
| + """
|
| +
|
| + def __init__(self, manifest_path, api_methods, base_dir):
|
| + """ Initializes a Sample instance given a path to a manifest.
|
| +
|
| + Args:
|
| + manifest_path: A filesystem path to a manifest file.
|
| + api_methods: A list of strings containing all possible Chrome extension
|
| + API calls.
|
| + base_dir: The base directory where this sample will be referenced from -
|
| + paths will be made relative to this directory.
|
| + """
|
| + self._base_dir = base_dir
|
| + self._manifest_path = manifest_path
|
| + self._manifest = parse_json_file(self._manifest_path)
|
| + self._locale_data = self._parse_locale_data()
|
| +
|
| + # The following properties will be serialized when converting this object
|
| + # to JSON.
|
| +
|
| + self['id'] = hashlib.sha1(manifest_path).hexdigest()
|
| + self['api_calls'] = self._parse_api_calls(api_methods)
|
| + self['name'] = self._parse_name()
|
| + self['description'] = self._parse_description()
|
| + self['icon'] = self._parse_icon()
|
| + self['features'] = self._parse_features()
|
| + self['protocols'] = self._parse_protocols()
|
| + self['path'] = self._get_relative_path()
|
| + self['search_string'] = self._get_search_string()
|
| + self['source_files'] = self._parse_source_files()
|
| +
|
| + _FEATURE_ATTRIBUTES = (
|
| + 'browser_action',
|
| + 'page_action',
|
| + 'background_page',
|
| + 'options_page',
|
| + 'plugins',
|
| + 'theme',
|
| + 'chrome_url_overrides'
|
| + )
|
| + """ Attributes that will map to "features" if their corresponding key is
|
| + present in the extension manifest. """
|
| +
|
| + _SOURCE_FILE_EXTENSIONS = ('.html', '.json', '.js', '.css', '.htm')
|
| + """ File extensions to files which may contain source code."""
|
| +
|
| + _ENGLISH_LOCALES = ['en_US', 'en', 'en_GB']
|
| + """ Locales from which translations may be used in the sample gallery. """
|
| +
|
| + def _get_localized_manifest_value(self, key):
|
| + """ Returns a localized version of the requested manifest value.
|
| +
|
| + Args:
|
| + key: The manifest key whose value the caller wants translated.
|
| +
|
| + Returns:
|
| + If the supplied value exists and contains a ___MSG_token___ value, this
|
| + method will resolve the appropriate translation and return the result.
|
| + If no token exists, the manifest value will be returned. If the key does
|
| + not exist, an empty string will be returned.
|
| +
|
| + Raises:
|
| + Exception: If the localized value for the given token could not be found.
|
| + """
|
| + if self._manifest.has_key(key):
|
| + if self._manifest[key][:6] == '__MSG_':
|
| + try:
|
| + return self._get_localized_value(self._manifest[key])
|
| + except Exception, msg:
|
| + raise Exception("Could not translate manifest value for key %s: %s" %
|
| + (key, msg))
|
| + else:
|
| + return self._manifest[key]
|
| + else:
|
| + return ''
|
| +
|
| + def _get_localized_value(self, message_token):
|
| + """ Returns the localized version of the requested MSG bundle token.
|
| +
|
| + Args:
|
| + message_token: A message bundle token like __MSG_extensionName__.
|
| +
|
| + Returns:
|
| + The translated text corresponding to the token, with any placeholders
|
| + automatically resolved and substituted in.
|
| +
|
| + Raises:
|
| + Exception: If a message bundle token is not found in the translations.
|
| + """
|
| + placeholder_pattern = re.compile('\$(\w*)\$')
|
| + token = message_token[6:-2]
|
| + if self._locale_data.has_key(token):
|
| + message = self._locale_data[token]['message']
|
| +
|
| + placeholder_match = placeholder_pattern.search(message)
|
| + if placeholder_match:
|
| + # There are placeholders in the translation - substitute them.
|
| + placeholder_name = placeholder_match.group(1)
|
| + placeholders = self._locale_data[token]['placeholders']
|
| + if placeholders.has_key(placeholder_name.lower()):
|
| + placeholder_value = placeholders[placeholder_name.lower()]['content']
|
| + placeholder_token = '$%s$' % placeholder_name
|
| + message = message.replace(placeholder_token, placeholder_value)
|
| + return message
|
| + else:
|
| + raise Exception('Could not find localized string: %s' % message_token)
|
| +
|
| + def _get_relative_path(self):
|
| + """ Returns a relative path from the supplied base dir to the manifest dir.
|
| +
|
| + This method is used because we may not be able to rely on os.path.relpath
|
| + which was introduced in Python 2.6 and only works on Windows and Unix.
|
| +
|
| + Since the example extensions should always be subdirectories of the
|
| + base sample manifest path, we can get a relative path through a simple
|
| + string substitution.
|
| +
|
| + Returns:
|
| + A relative directory path from the sample manifest's directory to the
|
| + directory containing this sample's manifest.json.
|
| + """
|
| + real_manifest_path = os.path.realpath(self._manifest_path)
|
| + real_base_path = os.path.realpath(self._base_dir)
|
| + return real_manifest_path.replace(real_base_path, '')\
|
| + .replace('manifest.json', '')[1:]
|
| +
|
| + def _get_search_string(self):
|
| + """ Constructs a string to be used when searching the samples list.
|
| +
|
| + To make the implementation of the JavaScript-based search very direct, a
|
| + string is constructed containing the title, description, API calls, and
|
| + features that this sample uses, and is converted to uppercase. This makes
|
| + JavaScript sample searching very fast and easy to implement.
|
| +
|
| + Returns:
|
| + An uppercase string containing information to match on for searching
|
| + samples on the client.
|
| + """
|
| + search_terms = [
|
| + self['name'],
|
| + self['description'],
|
| + ]
|
| + search_terms.extend(self['features'])
|
| + search_terms.extend(self['api_calls'])
|
| + search_string = ' '.join(search_terms).replace('"', '')\
|
| + .replace('\'', '')\
|
| + .upper()
|
| + return search_string
|
| +
|
| + def _parse_api_calls(self, api_methods):
|
| + """ Returns a list of Chrome extension API calls the sample makes.
|
| +
|
| + Parses any *.html and *.js files in the sample directory and matches them
|
| + against the supplied list of all available API methods, returning methods
|
| + which show up in the sample code.
|
| +
|
| + Args:
|
| + api_methods: A list of strings containing the potential
|
| + API calls the and the extension sample could be making.
|
| +
|
| + Returns:
|
| + A set of every member of api_methods that appears in any *.html or *.js
|
| + file contained in this sample's directory (or subdirectories).
|
| +
|
| + Raises:
|
| + Exception: If any of the *.html or *.js files cannot be read.
|
| + """
|
| + api_calls = set()
|
| + extension_dir_path = os.path.dirname(self._manifest_path)
|
| + for root, dirs, files in os.walk(extension_dir_path):
|
| + for file in files:
|
| + if file[-5:] == '.html' or file[-3:] == '.js':
|
| + path = os.path.join(root, file)
|
| + try:
|
| + code_file = open(path)
|
| + except IOError, msg:
|
| + raise Exception("Failed to read %s: %s" % (path, msg))
|
| + code_contents = code_file.read()
|
| + code_file.close()
|
| +
|
| + for method in api_methods:
|
| + if (code_contents.find(method) > -1):
|
| + api_calls.add(method)
|
| + return sorted(api_calls)
|
| +
|
| + def _parse_source_files(self):
|
| + """ Returns a list of paths to source files present in the extenion.
|
| +
|
| + Returns:
|
| + A list of paths relative to the manifest file directory.
|
| + """
|
| + source_paths = []
|
| + base_path = os.path.realpath(os.path.dirname(self._manifest_path))
|
| + for root, directories, files in os.walk(base_path):
|
| + if '.svn' in directories:
|
| + directories.remove('.svn') # Don't go into SVN metadata directories
|
| +
|
| + for file_name in files:
|
| + ext = os.path.splitext(file_name)[1]
|
| + if ext in self._SOURCE_FILE_EXTENSIONS:
|
| + path = os.path.realpath(os.path.join(root, file_name))
|
| + path = path.replace(base_path, '')[1:]
|
| + source_paths.append(path)
|
| + return source_paths
|
| +
|
| + def _parse_description(self):
|
| + """ Returns a localized description of the extension.
|
| +
|
| + Returns:
|
| + A localized version of the sample's description.
|
| + """
|
| + return self._get_localized_manifest_value('description')
|
| +
|
| + def _parse_features(self):
|
| + """ Returns a list of features the sample uses.
|
| +
|
| + Returns:
|
| + A list of features the extension uses, as determined by
|
| + self._FEATURE_ATTRIBUTES.
|
| + """
|
| + features = set()
|
| + for feature_attr in self._FEATURE_ATTRIBUTES:
|
| + if self._manifest.has_key(feature_attr):
|
| + features.add(feature_attr)
|
| +
|
| + if self._uses_popup():
|
| + features.add('popup')
|
| +
|
| + if self._manifest.has_key('permissions'):
|
| + for permission in self._manifest['permissions']:
|
| + split = permission.split('://')
|
| + if (len(split) == 1):
|
| + features.add(split[0])
|
| + return sorted(features)
|
| +
|
| + def _parse_icon(self):
|
| + """ Returns the path to the 128px icon for this sample.
|
| +
|
| + Returns:
|
| + The path to the 128px icon if defined in the manifest, None otherwise.
|
| + """
|
| + if (self._manifest.has_key('icons') and
|
| + self._manifest['icons'].has_key('128')):
|
| + return self._manifest['icons']['128']
|
| + else:
|
| + return None
|
| +
|
| + def _parse_locale_data(self):
|
| + """ Parses this sample's locale data into a dict.
|
| +
|
| + Because the sample gallery is in English, this method only looks for
|
| + translations as defined by self._ENGLISH_LOCALES.
|
| +
|
| + Returns:
|
| + A dict containing the translation keys and corresponding English text
|
| + for this extension.
|
| +
|
| + Raises:
|
| + Exception: If the messages file cannot be read, or if it is improperly
|
| + formatted JSON.
|
| + """
|
| + en_messages = {}
|
| + extension_dir_path = os.path.dirname(self._manifest_path)
|
| + for locale in self._ENGLISH_LOCALES:
|
| + en_messages_path = os.path.join(extension_dir_path, '_locales', locale,
|
| + 'messages.json')
|
| + if (os.path.isfile(en_messages_path)):
|
| + break
|
| +
|
| + if (os.path.isfile(en_messages_path)):
|
| + try:
|
| + en_messages_file = open(en_messages_path, 'r')
|
| + except IOError, msg:
|
| + raise Exception("Failed to read %s: %s" % (en_messages_path, msg))
|
| + en_messages_contents = en_messages_file.read()
|
| + en_messages_file.close()
|
| + try:
|
| + en_messages = json.loads(en_messages_contents)
|
| + except ValueError, msg:
|
| + raise Exception("File %s has a syntax error: %s" %
|
| + (en_messages_path, msg))
|
| + return en_messages
|
| +
|
| + def _parse_name(self):
|
| + """ Returns a localized name for the extension.
|
| +
|
| + Returns:
|
| + A localized version of the sample's name.
|
| + """
|
| + return self._get_localized_manifest_value('name')
|
| +
|
| + def _parse_protocols(self):
|
| + """ Returns a list of protocols this extension requests permission for.
|
| +
|
| + Returns:
|
| + A list of every unique protocol listed in the manifest's permssions.
|
| + """
|
| + protocols = []
|
| + if self._manifest.has_key('permissions'):
|
| + for permission in self._manifest['permissions']:
|
| + split = permission.split('://')
|
| + if (len(split) == 2) and (split[0] not in protocols):
|
| + protocols.append(split[0] + "://")
|
| + return protocols
|
| +
|
| + def _uses_background(self):
|
| + """ Returns true if the extension defines a background page. """
|
| + return self._manifest.has_key('background_page')
|
| +
|
| + def _uses_browser_action(self):
|
| + """ Returns true if the extension defines a browser action. """
|
| + return self._manifest.has_key('browser_action')
|
| +
|
| + def _uses_content_scripts(self):
|
| + """ Returns true if the extension uses content scripts. """
|
| + return self._manifest.has_key('content_scripts')
|
| +
|
| + def _uses_options(self):
|
| + """ Returns true if the extension defines an options page. """
|
| + return self._manifest.has_key('options_page')
|
| +
|
| + def _uses_page_action(self):
|
| + """ Returns true if the extension uses a page action. """
|
| + return self._manifest.has_key('page_action')
|
| +
|
| + def _uses_popup(self):
|
| + """ Returns true if the extension defines a popup on a page or browser
|
| + action. """
|
| + has_b_popup = (self._uses_browser_action() and
|
| + self._manifest['browser_action'].has_key('popup'))
|
| + has_p_popup = (self._uses_page_action() and
|
| + self._manifest['page_action'].has_key('popup'))
|
| + return has_b_popup or has_p_popup
|
| +
|
| + def is_app(self):
|
| + """ Returns true if the extension has an 'app' section in its manifest."""
|
| + return self._manifest.has_key('app')
|
|
|