"""
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