""" Greasemonkey compiler Compiles a Greasemonkey user script into a Firefox extension. Copyright 2005 Adrian Holovaty License: GPL Version: 0.1.2 Sample usage: >>> from greasemonkey import UserScript >>> e = UserScript(javascript=u'alert("Hello");', creator=u'Adrian Holovaty', version=u'0.1', guid=u'{9a86149f-23fc-4842-9dd8-4d8eb02fd055}', homepage=u'http://www.holovaty.com/') >>> f = open('test.xpi', 'w') >>> e.write_xpi(f) >>> f.close() # test.xpi in the current directory is now a Firefox extension. """ from xml.sax.handler import feature_namespaces from xml.sax.saxutils import XMLGenerator from StringIO import StringIO # Can't use cStringIO because of Unicode. import re, zipfile __version__ = '0.1.2' SCRIPT_OPTIONS_RE = re.compile(r'// ==UserScript==.*?((// @\S+\s+[^\n]+\n)+).*?// ==/UserScript==', re.DOTALL) class InvalidOptions(Exception): pass class InvalidExtension(Exception): pass class SimplerXMLGenerator(XMLGenerator): def addQuickElementNS(self, name, contents=None, attrs={}): "Convenience method for adding a namespaced element with no children." self.startElementNS(name, None, attrs) if contents is not None: self.characters(contents) self.endElementNS(name, None) def create_slug(name): return re.sub(r'[^\w]', '', name).lower() def get_script_options(src): """ Given user-script source code, returns a dictionary mapping each option name (without the '@') to a list of values. (A list is necessary because the "include" and "exclude" options can be set multiple times.) Raises InvalidOptions if no options are found. """ matches = SCRIPT_OPTIONS_RE.search(src) if not matches: raise InvalidOptions opts = {} for m in matches.group(1).split('\n'): if not m: continue bits = m[4:].split(None, 1) if len(bits) > 1: k, v = bits else: k, v = bits[0], '' opts.setdefault(k, []).append(v.strip()) return opts def get_regex_from_pattern(pattern): """ Returns the given search pattern converted to a JavaScript regular expression (as a string). This is a Python implementation of Greasemonkey's convert2RegExp(), with the exception that forward slashes are also escaped. """ regex = pattern.replace(' ', '') regex = regex.replace(r'\\', r'\\\\') for c in '.?^$+{[|()]/': regex = regex.replace(c, r'\%s' % c) regex = regex.replace('*', '.*') return '^%s$' % regex class UserScript: rdfns = u"http://www.w3.org/1999/02/22-rdf-syntax-ns#" emns = u"http://www.mozilla.org/2004/em-rdf#" chromens = u"http://www.mozilla.org/rdf/chrome#" def __init__(self, javascript, creator, version, guid, homepage=''): "Raises InvalidExtension for bad params." try: self.opts = get_script_options(javascript) except InvalidOptions: raise InvalidExtension, "Couldn't find any user-script options." try: self.name = self.opts['name'][0] except KeyError: raise InvalidExtension, "Script options didn't provide '@name'." try: self.description = self.opts['description'][0] except KeyError: raise InvalidExtension, "Script options didn't provide '@description'." if not self.opts.has_key('include'): raise InvalidExtension, "Script options didn't provide '@include'." self.creator, self.version = creator, version self.javascript, self.homepage = javascript, homepage self.guid = guid self.slug = create_slug(self.name) def get_domain_test_js(self, rule): """ Returns a string of JavaScript logic that gets evaluated to determine whether this script should run on a given URL, according to the given rule (either 'include' or 'exclude'). """ return ' || '.join(['/%s/.test(e.originalTarget.location.href)' % get_regex_from_pattern(rule) \ for rule in self.opts.get(rule, [])]) def get_install_rdf(self): "Returns the install.rdf file for this extension." f = StringIO() handler = SimplerXMLGenerator(f, 'utf-8') handler.startDocument() handler.startPrefixMapping(u"RDF", self.rdfns) handler.startPrefixMapping(u"em", self.emns) handler.startElementNS((self.rdfns, u"RDF"), None, {}) handler.startElementNS((self.rdfns, u"Description"), None, {(self.rdfns, u"about"): u"urn:mozilla:install-manifest"}) handler.addQuickElementNS((self.emns, u"name"), self.name) handler.addQuickElementNS((self.emns, u"id"), self.guid) handler.addQuickElementNS((self.emns, u"version"), self.version) handler.addQuickElementNS((self.emns, u"description"), self.description) handler.addQuickElementNS((self.emns, u"creator"), self.creator) handler.addQuickElementNS((self.emns, u"homepageURL"), self.homepage) handler.startElementNS((self.emns, u"targetApplication"), None, {}) handler.startElementNS((self.rdfns, u"Description"), None, {}) handler.addQuickElementNS((self.emns, u"id"), u"{ec8030f7-c20a-464f-9b0e-13a3a9e97384}") handler.addQuickElementNS((self.emns, u"minVersion"), u"0.9") handler.addQuickElementNS((self.emns, u"maxVersion"), u"1.0") handler.endElementNS((self.rdfns, u"Description"), None) handler.endElementNS((self.emns, u"targetApplication"), None) handler.startElementNS((self.emns, u"file"), None, {}) handler.startElementNS((self.rdfns, u"Description"), None, {(self.rdfns, u"about"): u"urn:mozilla:extension:file:%s" % self.slug}) handler.addQuickElementNS((self.emns, u"package"), u"content/") handler.endElementNS((self.rdfns, u"Description"), None) handler.endElementNS((self.emns, u"file"), None) handler.endElementNS((self.rdfns, u"Description"), None) handler.endElementNS((self.rdfns, u"RDF"), None) return f.getvalue() def get_contents_rdf(self): "Returns the contents.rdf file for this extension." f = StringIO() handler = SimplerXMLGenerator(f, 'utf-8') handler.startDocument() handler.startPrefixMapping(u"RDF", self.rdfns) handler.startPrefixMapping(u"chrome", self.chromens) handler.startElementNS((self.rdfns, u"RDF"), None, {}) handler.startElementNS((self.rdfns, u"Seq"), None, {(self.rdfns, u"about"): u"urn:mozilla:package:root"}) handler.addQuickElementNS((self.rdfns, u"li"), None, {(self.rdfns, u"resource"): u"urn:mozilla:package:%s" % self.slug}) handler.endElementNS((self.rdfns, u"Seq"), None) handler.addQuickElementNS((self.rdfns, u"Description"), None, {(self.rdfns, u"about"): u"urn:mozilla:package:%s" % self.slug, (self.chromens, u"displayName"): self.name, (self.chromens, u"author"): self.creator, (self.chromens, u"authorURL"): self.homepage, (self.chromens, u"name"): self.slug, (self.chromens, u"extension"): u"true", (self.chromens, u"description"): self.description}) handler.startElementNS((self.rdfns, u"Seq"), None, {(self.rdfns, u"about"): u"urn:mozilla:overlays"}) handler.addQuickElementNS((self.rdfns, u"li"), None, {(self.rdfns, u"resource"): u"chrome://browser/content/browser.xul"}) handler.endElementNS((self.rdfns, u"Seq"), None) handler.startElementNS((self.rdfns, u"Seq"), None, {(self.rdfns, u"about"): u"chrome://browser/content/browser.xul"}) handler.addQuickElementNS((self.rdfns, u"li"), u"chrome://%s/content/browser.xul" % self.slug) handler.endElementNS((self.rdfns, u"Seq"), None) handler.endElementNS((self.rdfns, u"RDF"), None) return f.getvalue() def get_browser_xul(self): "Returns the browser.xul file for this extension." f = StringIO() handler = SimplerXMLGenerator(f, 'utf-8') handler.startDocument() handler.startElement(u"overlay", {u"id": u"%s-overlay" % self.slug, u"xmlns": u"http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"}) handler.startElement(u"script", {u"type": u"application/x-javascript"}) include_js = self.get_domain_test_js('include') exclude_js = self.get_domain_test_js('exclude') handler.ignorableWhitespace(ur""" """ % { 'slug': self.slug, 'include': include_js, 'exclude': exclude_js and 'if (!(%s)) {' % exclude_js or '', 'exclude_close': exclude_js and '}' or '' }) handler.endElement(u"script") handler.endElement(u"overlay") return f.getvalue() def write_xpi(self, f): """ Creates an XPI file for this extension and saves it in the given file-like object. """ z = zipfile.ZipFile(f, 'w') z.writestr('install.rdf', self.get_install_rdf().encode('utf-8')) # TODO: Check that slug doesn't have funky Unicode chars in it, # because zip files can't have those chars. z.writestr('chrome/%s/content/contents.rdf' % self.slug.encode('utf-8'), self.get_contents_rdf().encode('utf-8')) z.writestr('chrome/%s/content/browser.xul' % self.slug.encode('utf-8'), self.get_browser_xul().encode('utf-8')) z.writestr('chrome/%s/content/javascript.js' % self.slug.encode('utf-8'), self.javascript.encode('utf-8')) z.close() if __name__ == "__main__": test_javascript = ur''' // Google Suggest Greasemonkey script // version 0.3 // 2005-04-01 // Released under the GPL license: http://www.gnu.org/copyleft/gpl.html // See http://www.holovaty.com/blog/archive/2005/03/19/1826 // // ==UserScript== // @name Google Suggest // @namespace http://www.holovaty.com/code/firefox/greasemonkey/ // @description Adds Google Suggest dropdown to normal Google searches // @include http://*.google.*/* // ==/UserScript== (function() { get_search_form = function() { return document.evaluate("//form[@action='/search']", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; } // Find the search form. If it doesn't exist on this page, don't bother. if (!get_search_form()) return; // "Import" Google Suggest JavaScript by dynamically appending it to the // current page as a