summaryrefslogtreecommitdiff
path: root/vendor/tornado
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/tornado')
-rw-r--r--vendor/tornado/MANIFEST.in2
-rw-r--r--vendor/tornado/README27
-rw-r--r--vendor/tornado/demos/appengine/README48
-rw-r--r--vendor/tornado/demos/appengine/app.yaml11
-rw-r--r--vendor/tornado/demos/appengine/blog.py169
-rw-r--r--vendor/tornado/demos/appengine/markdown.py1877
-rw-r--r--vendor/tornado/demos/appengine/static/blog.css153
-rw-r--r--vendor/tornado/demos/appengine/templates/archive.html31
-rw-r--r--vendor/tornado/demos/appengine/templates/base.html29
-rw-r--r--vendor/tornado/demos/appengine/templates/compose.html42
-rw-r--r--vendor/tornado/demos/appengine/templates/entry.html5
-rw-r--r--vendor/tornado/demos/appengine/templates/feed.xml26
-rw-r--r--vendor/tornado/demos/appengine/templates/home.html8
-rw-r--r--vendor/tornado/demos/appengine/templates/modules/entry.html8
-rwxr-xr-xvendor/tornado/demos/auth/authdemo.py79
-rw-r--r--vendor/tornado/demos/blog/README57
-rwxr-xr-xvendor/tornado/demos/blog/blog.py195
-rw-r--r--vendor/tornado/demos/blog/markdown.py1877
-rw-r--r--vendor/tornado/demos/blog/schema.sql44
-rw-r--r--vendor/tornado/demos/blog/static/blog.css153
-rw-r--r--vendor/tornado/demos/blog/templates/archive.html31
-rw-r--r--vendor/tornado/demos/blog/templates/base.html27
-rw-r--r--vendor/tornado/demos/blog/templates/compose.html42
-rw-r--r--vendor/tornado/demos/blog/templates/entry.html5
-rw-r--r--vendor/tornado/demos/blog/templates/feed.xml26
-rw-r--r--vendor/tornado/demos/blog/templates/home.html8
-rw-r--r--vendor/tornado/demos/blog/templates/modules/entry.html8
-rwxr-xr-xvendor/tornado/demos/chat/chatdemo.py156
-rw-r--r--vendor/tornado/demos/chat/static/chat.css56
-rw-r--r--vendor/tornado/demos/chat/static/chat.js135
-rw-r--r--vendor/tornado/demos/chat/templates/index.html37
-rw-r--r--vendor/tornado/demos/chat/templates/message.html1
-rw-r--r--vendor/tornado/demos/facebook/README8
-rwxr-xr-xvendor/tornado/demos/facebook/facebook.py127
-rw-r--r--vendor/tornado/demos/facebook/static/facebook.css97
-rw-r--r--vendor/tornado/demos/facebook/static/facebook.js0
-rw-r--r--vendor/tornado/demos/facebook/templates/modules/post.html29
-rw-r--r--vendor/tornado/demos/facebook/templates/stream.html22
-rw-r--r--vendor/tornado/demos/facebook/uimodules.py22
-rwxr-xr-xvendor/tornado/demos/helloworld/helloworld.py43
-rw-r--r--vendor/tornado/setup.py44
-rw-r--r--vendor/tornado/tornado/__init__.py17
-rw-r--r--vendor/tornado/tornado/auth.py883
-rw-r--r--vendor/tornado/tornado/autoreload.py95
-rw-r--r--vendor/tornado/tornado/database.py180
-rw-r--r--vendor/tornado/tornado/epoll.c112
-rw-r--r--vendor/tornado/tornado/escape.py112
-rw-r--r--vendor/tornado/tornado/httpclient.py465
-rw-r--r--vendor/tornado/tornado/httpserver.py450
-rw-r--r--vendor/tornado/tornado/ioloop.py483
-rw-r--r--vendor/tornado/tornado/iostream.py229
-rw-r--r--vendor/tornado/tornado/locale.py457
-rw-r--r--vendor/tornado/tornado/options.py386
-rw-r--r--vendor/tornado/tornado/s3server.py255
-rw-r--r--vendor/tornado/tornado/template.py576
-rw-r--r--vendor/tornado/tornado/test/README4
-rwxr-xr-xvendor/tornado/tornado/test/test_ioloop.py38
-rw-r--r--vendor/tornado/tornado/web.py1445
-rw-r--r--vendor/tornado/tornado/websocket.py138
-rw-r--r--vendor/tornado/tornado/win32_support.py123
-rw-r--r--vendor/tornado/tornado/wsgi.py311
-rw-r--r--vendor/tornado/website/app.yaml15
-rw-r--r--vendor/tornado/website/index.yaml0
-rw-r--r--vendor/tornado/website/markdown/__init__.py603
-rw-r--r--vendor/tornado/website/markdown/blockparser.py95
-rw-r--r--vendor/tornado/website/markdown/blockprocessors.py460
-rw-r--r--vendor/tornado/website/markdown/commandline.py96
-rw-r--r--vendor/tornado/website/markdown/etree_loader.py33
-rw-r--r--vendor/tornado/website/markdown/extensions/__init__.py0
-rw-r--r--vendor/tornado/website/markdown/extensions/toc.py140
-rw-r--r--vendor/tornado/website/markdown/html4.py274
-rw-r--r--vendor/tornado/website/markdown/inlinepatterns.py371
-rw-r--r--vendor/tornado/website/markdown/odict.py162
-rw-r--r--vendor/tornado/website/markdown/postprocessors.py77
-rw-r--r--vendor/tornado/website/markdown/preprocessors.py214
-rw-r--r--vendor/tornado/website/markdown/treeprocessors.py329
-rw-r--r--vendor/tornado/website/static/base.css120
-rwxr-xr-xvendor/tornado/website/static/facebook.pngbin0 -> 7457 bytes
-rwxr-xr-xvendor/tornado/website/static/friendfeed.pngbin0 -> 7906 bytes
-rw-r--r--vendor/tornado/website/static/robots.txt2
-rw-r--r--vendor/tornado/website/static/tornado-0.1.tar.gzbin0 -> 106878 bytes
-rw-r--r--vendor/tornado/website/static/tornado-0.2.tar.gzbin0 -> 200680 bytes
-rw-r--r--vendor/tornado/website/static/tornado.pngbin0 -> 7101 bytes
-rwxr-xr-xvendor/tornado/website/static/twitter.pngbin0 -> 7197 bytes
-rw-r--r--vendor/tornado/website/templates/base.html27
-rw-r--r--vendor/tornado/website/templates/documentation.html9
-rw-r--r--vendor/tornado/website/templates/documentation.txt866
-rw-r--r--vendor/tornado/website/templates/index.html51
-rw-r--r--vendor/tornado/website/website.py63
89 files changed, 16531 insertions, 0 deletions
diff --git a/vendor/tornado/MANIFEST.in b/vendor/tornado/MANIFEST.in
new file mode 100644
index 0000000000..c7a51e4094
--- /dev/null
+++ b/vendor/tornado/MANIFEST.in
@@ -0,0 +1,2 @@
+recursive-include demos *.py *.yaml *.html *.css *.png *.js *.xml *.sql README
+include tornado/epoll.c
diff --git a/vendor/tornado/README b/vendor/tornado/README
new file mode 100644
index 0000000000..d504022243
--- /dev/null
+++ b/vendor/tornado/README
@@ -0,0 +1,27 @@
+Tornado
+=======
+Tornado is an open source version of the scalable, non-blocking web server
+and and tools that power FriendFeed. Documentation and downloads are
+available at http://www.tornadoweb.org/
+
+Tornado is licensed under the Apache Licence, Version 2.0
+(http://www.apache.org/licenses/LICENSE-2.0.html).
+
+Installation
+============
+To install:
+
+ python setup.py build
+ sudo python setup.py install
+
+Tornado has been tested on Python 2.5 and 2.6. To use all of the features
+of Tornado, you need to have PycURL and a JSON library like simplejson
+installed.
+
+On Mac OS X, you can install the packages with:
+
+ sudo easy_install setuptools pycurl==7.16.2.1 simplejson
+
+On Ubuntu Linux, you can install the packages with:
+
+ sudo apt-get install python-pycurl python-simplejson
diff --git a/vendor/tornado/demos/appengine/README b/vendor/tornado/demos/appengine/README
new file mode 100644
index 0000000000..e4aead6701
--- /dev/null
+++ b/vendor/tornado/demos/appengine/README
@@ -0,0 +1,48 @@
+Running the Tornado AppEngine example
+=====================================
+This example is designed to run in Google AppEngine, so there are a couple
+of steps to get it running. You can download the Google AppEngine Python
+development environment at http://code.google.com/appengine/downloads.html.
+
+1. Link or copy the tornado code directory into this directory:
+
+ ln -s ../../tornado tornado
+
+ AppEngine doesn't use the Python modules installed on this machine.
+ You need to have the 'tornado' module copied or linked for AppEngine
+ to find it.
+
+3. Install and run dev_appserver
+
+ If you don't already have the App Engine SDK, download it from
+ http://code.google.com/appengine/downloads.html
+
+ To start the tornado demo, run the dev server on this directory:
+
+ dev_appserver.py .
+
+4. Visit http://localhost:8080/ in your browser
+
+ If you sign in as an administrator, you will be able to create and
+ edit blog posts. If you sign in as anybody else, you will only see
+ the existing blog posts.
+
+
+If you want to deploy the blog in production:
+
+1. Register a new appengine application and put its id in app.yaml
+
+ First register a new application at http://appengine.google.com/.
+ Then edit app.yaml in this directory and change the "application"
+ setting from "tornado-appenginge" to your new application id.
+
+2. Deploy to App Engine
+
+ If you registered an application id, you can now upload your new
+ Tornado blog by running this command:
+
+ appcfg update .
+
+ After that, visit application_id.appspot.com, where application_id
+ is the application you registered.
+
diff --git a/vendor/tornado/demos/appengine/app.yaml b/vendor/tornado/demos/appengine/app.yaml
new file mode 100644
index 0000000000..2d00c586dd
--- /dev/null
+++ b/vendor/tornado/demos/appengine/app.yaml
@@ -0,0 +1,11 @@
+application: tornado-appengine
+version: 1
+runtime: python
+api_version: 1
+
+handlers:
+- url: /static/
+ static_dir: static
+
+- url: /.*
+ script: blog.py
diff --git a/vendor/tornado/demos/appengine/blog.py b/vendor/tornado/demos/appengine/blog.py
new file mode 100644
index 0000000000..ccaabd5392
--- /dev/null
+++ b/vendor/tornado/demos/appengine/blog.py
@@ -0,0 +1,169 @@
+#!/usr/bin/env python
+#
+# Copyright 2009 Facebook
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import functools
+import markdown
+import os.path
+import re
+import tornado.web
+import tornado.wsgi
+import unicodedata
+import wsgiref.handlers
+
+from google.appengine.api import users
+from google.appengine.ext import db
+
+
+class Entry(db.Model):
+ """A single blog entry."""
+ author = db.UserProperty()
+ title = db.StringProperty(required=True)
+ slug = db.StringProperty(required=True)
+ markdown = db.TextProperty(required=True)
+ html = db.TextProperty(required=True)
+ published = db.DateTimeProperty(auto_now_add=True)
+ updated = db.DateTimeProperty(auto_now=True)
+
+
+def administrator(method):
+ """Decorate with this method to restrict to site admins."""
+ @functools.wraps(method)
+ def wrapper(self, *args, **kwargs):
+ if not self.current_user:
+ if self.request.method == "GET":
+ self.redirect(self.get_login_url())
+ return
+ raise tornado.web.HTTPError(403)
+ elif not self.current_user.administrator:
+ if self.request.method == "GET":
+ self.redirect("/")
+ return
+ raise tornado.web.HTTPError(403)
+ else:
+ return method(self, *args, **kwargs)
+ return wrapper
+
+
+class BaseHandler(tornado.web.RequestHandler):
+ """Implements Google Accounts authentication methods."""
+ def get_current_user(self):
+ user = users.get_current_user()
+ if user: user.administrator = users.is_current_user_admin()
+ return user
+
+ def get_login_url(self):
+ return users.create_login_url(self.request.uri)
+
+ def render_string(self, template_name, **kwargs):
+ # Let the templates access the users module to generate login URLs
+ return tornado.web.RequestHandler.render_string(
+ self, template_name, users=users, **kwargs)
+
+
+class HomeHandler(BaseHandler):
+ def get(self):
+ entries = db.Query(Entry).order('-published').fetch(limit=5)
+ if not entries:
+ if not self.current_user or self.current_user.administrator:
+ self.redirect("/compose")
+ return
+ self.render("home.html", entries=entries)
+
+
+class EntryHandler(BaseHandler):
+ def get(self, slug):
+ entry = db.Query(Entry).filter("slug =", slug).get()
+ if not entry: raise tornado.web.HTTPError(404)
+ self.render("entry.html", entry=entry)
+
+
+class ArchiveHandler(BaseHandler):
+ def get(self):
+ entries = db.Query(Entry).order('-published')
+ self.render("archive.html", entries=entries)
+
+
+class FeedHandler(BaseHandler):
+ def get(self):
+ entries = db.Query(Entry).order('-published').fetch(limit=10)
+ self.set_header("Content-Type", "application/atom+xml")
+ self.render("feed.xml", entries=entries)
+
+
+class ComposeHandler(BaseHandler):
+ @administrator
+ def get(self):
+ key = self.get_argument("key", None)
+ entry = Entry.get(key) if key else None
+ self.render("compose.html", entry=entry)
+
+ @administrator
+ def post(self):
+ key = self.get_argument("key", None)
+ if key:
+ entry = Entry.get(key)
+ entry.title = self.get_argument("title")
+ entry.markdown = self.get_argument("markdown")
+ entry.html = markdown.markdown(self.get_argument("markdown"))
+ else:
+ title = self.get_argument("title")
+ slug = unicodedata.normalize("NFKD", title).encode(
+ "ascii", "ignore")
+ slug = re.sub(r"[^\w]+", " ", slug)
+ slug = "-".join(slug.lower().strip().split())
+ if not slug: slug = "entry"
+ while True:
+ existing = db.Query(Entry).filter("slug =", slug).get()
+ if not existing or str(existing.key()) == key:
+ break
+ slug += "-2"
+ entry = Entry(
+ author=self.current_user,
+ title=title,
+ slug=slug,
+ markdown=self.get_argument("markdown"),
+ html=markdown.markdown(self.get_argument("markdown")),
+ )
+ entry.put()
+ self.redirect("/entry/" + entry.slug)
+
+
+class EntryModule(tornado.web.UIModule):
+ def render(self, entry):
+ return self.render_string("modules/entry.html", entry=entry)
+
+
+settings = {
+ "blog_title": u"Tornado Blog",
+ "template_path": os.path.join(os.path.dirname(__file__), "templates"),
+ "ui_modules": {"Entry": EntryModule},
+ "xsrf_cookies": True,
+}
+application = tornado.wsgi.WSGIApplication([
+ (r"/", HomeHandler),
+ (r"/archive", ArchiveHandler),
+ (r"/feed", FeedHandler),
+ (r"/entry/([^/]+)", EntryHandler),
+ (r"/compose", ComposeHandler),
+], **settings)
+
+
+def main():
+ wsgiref.handlers.CGIHandler().run(application)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/vendor/tornado/demos/appengine/markdown.py b/vendor/tornado/demos/appengine/markdown.py
new file mode 100644
index 0000000000..59ba731bf0
--- /dev/null
+++ b/vendor/tornado/demos/appengine/markdown.py
@@ -0,0 +1,1877 @@
+#!/usr/bin/env python
+# Copyright (c) 2007-2008 ActiveState Corp.
+# License: MIT (http://www.opensource.org/licenses/mit-license.php)
+
+r"""A fast and complete Python implementation of Markdown.
+
+[from http://daringfireball.net/projects/markdown/]
+> Markdown is a text-to-HTML filter; it translates an easy-to-read /
+> easy-to-write structured text format into HTML. Markdown's text
+> format is most similar to that of plain text email, and supports
+> features such as headers, *emphasis*, code blocks, blockquotes, and
+> links.
+>
+> Markdown's syntax is designed not as a generic markup language, but
+> specifically to serve as a front-end to (X)HTML. You can use span-level
+> HTML tags anywhere in a Markdown document, and you can use block level
+> HTML tags (like <div> and <table> as well).
+
+Module usage:
+
+ >>> import markdown2
+ >>> markdown2.markdown("*boo!*") # or use `html = markdown_path(PATH)`
+ u'<p><em>boo!</em></p>\n'
+
+ >>> markdowner = Markdown()
+ >>> markdowner.convert("*boo!*")
+ u'<p><em>boo!</em></p>\n'
+ >>> markdowner.convert("**boom!**")
+ u'<p><strong>boom!</strong></p>\n'
+
+This implementation of Markdown implements the full "core" syntax plus a
+number of extras (e.g., code syntax coloring, footnotes) as described on
+<http://code.google.com/p/python-markdown2/wiki/Extras>.
+"""
+
+cmdln_desc = """A fast and complete Python implementation of Markdown, a
+text-to-HTML conversion tool for web writers.
+"""
+
+# Dev Notes:
+# - There is already a Python markdown processor
+# (http://www.freewisdom.org/projects/python-markdown/).
+# - Python's regex syntax doesn't have '\z', so I'm using '\Z'. I'm
+# not yet sure if there implications with this. Compare 'pydoc sre'
+# and 'perldoc perlre'.
+
+__version_info__ = (1, 0, 1, 14) # first three nums match Markdown.pl
+__version__ = '1.0.1.14'
+__author__ = "Trent Mick"
+
+import os
+import sys
+from pprint import pprint
+import re
+import logging
+try:
+ from hashlib import md5
+except ImportError:
+ from md5 import md5
+import optparse
+from random import random
+import codecs
+
+
+
+#---- Python version compat
+
+if sys.version_info[:2] < (2,4):
+ from sets import Set as set
+ def reversed(sequence):
+ for i in sequence[::-1]:
+ yield i
+ def _unicode_decode(s, encoding, errors='xmlcharrefreplace'):
+ return unicode(s, encoding, errors)
+else:
+ def _unicode_decode(s, encoding, errors='strict'):
+ return s.decode(encoding, errors)
+
+
+#---- globals
+
+DEBUG = False
+log = logging.getLogger("markdown")
+
+DEFAULT_TAB_WIDTH = 4
+
+# Table of hash values for escaped characters:
+def _escape_hash(s):
+ # Lame attempt to avoid possible collision with someone actually
+ # using the MD5 hexdigest of one of these chars in there text.
+ # Other ideas: random.random(), uuid.uuid()
+ #return md5(s).hexdigest() # Markdown.pl effectively does this.
+ return 'md5-'+md5(s).hexdigest()
+g_escape_table = dict([(ch, _escape_hash(ch))
+ for ch in '\\`*_{}[]()>#+-.!'])
+
+
+
+#---- exceptions
+
+class MarkdownError(Exception):
+ pass
+
+
+
+#---- public api
+
+def markdown_path(path, encoding="utf-8",
+ html4tags=False, tab_width=DEFAULT_TAB_WIDTH,
+ safe_mode=None, extras=None, link_patterns=None,
+ use_file_vars=False):
+ text = codecs.open(path, 'r', encoding).read()
+ return Markdown(html4tags=html4tags, tab_width=tab_width,
+ safe_mode=safe_mode, extras=extras,
+ link_patterns=link_patterns,
+ use_file_vars=use_file_vars).convert(text)
+
+def markdown(text, html4tags=False, tab_width=DEFAULT_TAB_WIDTH,
+ safe_mode=None, extras=None, link_patterns=None,
+ use_file_vars=False):
+ return Markdown(html4tags=html4tags, tab_width=tab_width,
+ safe_mode=safe_mode, extras=extras,
+ link_patterns=link_patterns,
+ use_file_vars=use_file_vars).convert(text)
+
+class Markdown(object):
+ # The dict of "extras" to enable in processing -- a mapping of
+ # extra name to argument for the extra. Most extras do not have an
+ # argument, in which case the value is None.
+ #
+ # This can be set via (a) subclassing and (b) the constructor
+ # "extras" argument.
+ extras = None
+
+ urls = None
+ titles = None
+ html_blocks = None
+ html_spans = None
+ html_removed_text = "[HTML_REMOVED]" # for compat with markdown.py
+
+ # Used to track when we're inside an ordered or unordered list
+ # (see _ProcessListItems() for details):
+ list_level = 0
+
+ _ws_only_line_re = re.compile(r"^[ \t]+$", re.M)
+
+ def __init__(self, html4tags=False, tab_width=4, safe_mode=None,
+ extras=None, link_patterns=None, use_file_vars=False):
+ if html4tags:
+ self.empty_element_suffix = ">"
+ else:
+ self.empty_element_suffix = " />"
+ self.tab_width = tab_width
+
+ # For compatibility with earlier markdown2.py and with
+ # markdown.py's safe_mode being a boolean,
+ # safe_mode == True -> "replace"
+ if safe_mode is True:
+ self.safe_mode = "replace"
+ else:
+ self.safe_mode = safe_mode
+
+ if self.extras is None:
+ self.extras = {}
+ elif not isinstance(self.extras, dict):
+ self.extras = dict([(e, None) for e in self.extras])
+ if extras:
+ if not isinstance(extras, dict):
+ extras = dict([(e, None) for e in extras])
+ self.extras.update(extras)
+ assert isinstance(self.extras, dict)
+ self._instance_extras = self.extras.copy()
+ self.link_patterns = link_patterns
+ self.use_file_vars = use_file_vars
+ self._outdent_re = re.compile(r'^(\t|[ ]{1,%d})' % tab_width, re.M)
+
+ def reset(self):
+ self.urls = {}
+ self.titles = {}
+ self.html_blocks = {}
+ self.html_spans = {}
+ self.list_level = 0
+ self.extras = self._instance_extras.copy()
+ if "footnotes" in self.extras:
+ self.footnotes = {}
+ self.footnote_ids = []
+
+ def convert(self, text):
+ """Convert the given text."""
+ # Main function. The order in which other subs are called here is
+ # essential. Link and image substitutions need to happen before
+ # _EscapeSpecialChars(), so that any *'s or _'s in the <a>
+ # and <img> tags get encoded.
+
+ # Clear the global hashes. If we don't clear these, you get conflicts
+ # from other articles when generating a page which contains more than
+ # one article (e.g. an index page that shows the N most recent
+ # articles):
+ self.reset()
+
+ if not isinstance(text, unicode):
+ #TODO: perhaps shouldn't presume UTF-8 for string input?
+ text = unicode(text, 'utf-8')
+
+ if self.use_file_vars:
+ # Look for emacs-style file variable hints.
+ emacs_vars = self._get_emacs_vars(text)
+ if "markdown-extras" in emacs_vars:
+ splitter = re.compile("[ ,]+")
+ for e in splitter.split(emacs_vars["markdown-extras"]):
+ if '=' in e:
+ ename, earg = e.split('=', 1)
+ try:
+ earg = int(earg)
+ except ValueError:
+ pass
+ else:
+ ename, earg = e, None
+ self.extras[ename] = earg
+
+ # Standardize line endings:
+ text = re.sub("\r\n|\r", "\n", text)
+
+ # Make sure $text ends with a couple of newlines:
+ text += "\n\n"
+
+ # Convert all tabs to spaces.
+ text = self._detab(text)
+
+ # Strip any lines consisting only of spaces and tabs.
+ # This makes subsequent regexen easier to write, because we can
+ # match consecutive blank lines with /\n+/ instead of something
+ # contorted like /[ \t]*\n+/ .
+ text = self._ws_only_line_re.sub("", text)
+
+ if self.safe_mode:
+ text = self._hash_html_spans(text)
+
+ # Turn block-level HTML blocks into hash entries
+ text = self._hash_html_blocks(text, raw=True)
+
+ # Strip link definitions, store in hashes.
+ if "footnotes" in self.extras:
+ # Must do footnotes first because an unlucky footnote defn
+ # looks like a link defn:
+ # [^4]: this "looks like a link defn"
+ text = self._strip_footnote_definitions(text)
+ text = self._strip_link_definitions(text)
+
+ text = self._run_block_gamut(text)
+
+ if "footnotes" in self.extras:
+ text = self._add_footnotes(text)
+
+ text = self._unescape_special_chars(text)
+
+ if self.safe_mode:
+ text = self._unhash_html_spans(text)
+
+ text += "\n"
+ return text
+
+ _emacs_oneliner_vars_pat = re.compile(r"-\*-\s*([^\r\n]*?)\s*-\*-", re.UNICODE)
+ # This regular expression is intended to match blocks like this:
+ # PREFIX Local Variables: SUFFIX
+ # PREFIX mode: Tcl SUFFIX
+ # PREFIX End: SUFFIX
+ # Some notes:
+ # - "[ \t]" is used instead of "\s" to specifically exclude newlines
+ # - "(\r\n|\n|\r)" is used instead of "$" because the sre engine does
+ # not like anything other than Unix-style line terminators.
+ _emacs_local_vars_pat = re.compile(r"""^
+ (?P<prefix>(?:[^\r\n|\n|\r])*?)
+ [\ \t]*Local\ Variables:[\ \t]*
+ (?P<suffix>.*?)(?:\r\n|\n|\r)
+ (?P<content>.*?\1End:)
+ """, re.IGNORECASE | re.MULTILINE | re.DOTALL | re.VERBOSE)
+
+ def _get_emacs_vars(self, text):
+ """Return a dictionary of emacs-style local variables.
+
+ Parsing is done loosely according to this spec (and according to
+ some in-practice deviations from this):
+ http://www.gnu.org/software/emacs/manual/html_node/emacs/Specifying-File-Variables.html#Specifying-File-Variables
+ """
+ emacs_vars = {}
+ SIZE = pow(2, 13) # 8kB
+
+ # Search near the start for a '-*-'-style one-liner of variables.
+ head = text[:SIZE]
+ if "-*-" in head:
+ match = self._emacs_oneliner_vars_pat.search(head)
+ if match:
+ emacs_vars_str = match.group(1)
+ assert '\n' not in emacs_vars_str
+ emacs_var_strs = [s.strip() for s in emacs_vars_str.split(';')
+ if s.strip()]
+ if len(emacs_var_strs) == 1 and ':' not in emacs_var_strs[0]:
+ # While not in the spec, this form is allowed by emacs:
+ # -*- Tcl -*-
+ # where the implied "variable" is "mode". This form
+ # is only allowed if there are no other variables.
+ emacs_vars["mode"] = emacs_var_strs[0].strip()
+ else:
+ for emacs_var_str in emacs_var_strs:
+ try:
+ variable, value = emacs_var_str.strip().split(':', 1)
+ except ValueError:
+ log.debug("emacs variables error: malformed -*- "
+ "line: %r", emacs_var_str)
+ continue
+ # Lowercase the variable name because Emacs allows "Mode"
+ # or "mode" or "MoDe", etc.
+ emacs_vars[variable.lower()] = value.strip()
+
+ tail = text[-SIZE:]
+ if "Local Variables" in tail:
+ match = self._emacs_local_vars_pat.search(tail)
+ if match:
+ prefix = match.group("prefix")
+ suffix = match.group("suffix")
+ lines = match.group("content").splitlines(0)
+ #print "prefix=%r, suffix=%r, content=%r, lines: %s"\
+ # % (prefix, suffix, match.group("content"), lines)
+
+ # Validate the Local Variables block: proper prefix and suffix
+ # usage.
+ for i, line in enumerate(lines):
+ if not line.startswith(prefix):
+ log.debug("emacs variables error: line '%s' "
+ "does not use proper prefix '%s'"
+ % (line, prefix))
+ return {}
+ # Don't validate suffix on last line. Emacs doesn't care,
+ # neither should we.
+ if i != len(lines)-1 and not line.endswith(suffix):
+ log.debug("emacs variables error: line '%s' "
+ "does not use proper suffix '%s'"
+ % (line, suffix))
+ return {}
+
+ # Parse out one emacs var per line.
+ continued_for = None
+ for line in lines[:-1]: # no var on the last line ("PREFIX End:")
+ if prefix: line = line[len(prefix):] # strip prefix
+ if suffix: line = line[:-len(suffix)] # strip suffix
+ line = line.strip()
+ if continued_for:
+ variable = continued_for
+ if line.endswith('\\'):
+ line = line[:-1].rstrip()
+ else:
+ continued_for = None
+ emacs_vars[variable] += ' ' + line
+ else:
+ try:
+ variable, value = line.split(':', 1)
+ except ValueError:
+ log.debug("local variables error: missing colon "
+ "in local variables entry: '%s'" % line)
+ continue
+ # Do NOT lowercase the variable name, because Emacs only
+ # allows "mode" (and not "Mode", "MoDe", etc.) in this block.
+ value = value.strip()
+ if value.endswith('\\'):
+ value = value[:-1].rstrip()
+ continued_for = variable
+ else:
+ continued_for = None
+ emacs_vars[variable] = value
+
+ # Unquote values.
+ for var, val in emacs_vars.items():
+ if len(val) > 1 and (val.startswith('"') and val.endswith('"')
+ or val.startswith('"') and val.endswith('"')):
+ emacs_vars[var] = val[1:-1]
+
+ return emacs_vars
+
+ # Cribbed from a post by Bart Lateur:
+ # <http://www.nntp.perl.org/group/perl.macperl.anyperl/154>
+ _detab_re = re.compile(r'(.*?)\t', re.M)
+ def _detab_sub(self, match):
+ g1 = match.group(1)
+ return g1 + (' ' * (self.tab_width - len(g1) % self.tab_width))
+ def _detab(self, text):
+ r"""Remove (leading?) tabs from a file.
+
+ >>> m = Markdown()
+ >>> m._detab("\tfoo")
+ ' foo'
+ >>> m._detab(" \tfoo")
+ ' foo'
+ >>> m._detab("\t foo")
+ ' foo'
+ >>> m._detab(" foo")
+ ' foo'
+ >>> m._detab(" foo\n\tbar\tblam")
+ ' foo\n bar blam'
+ """
+ if '\t' not in text:
+ return text
+ return self._detab_re.subn(self._detab_sub, text)[0]
+
+ _block_tags_a = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math|ins|del'
+ _strict_tag_block_re = re.compile(r"""
+ ( # save in \1
+ ^ # start of line (with re.M)
+ <(%s) # start tag = \2
+ \b # word break
+ (.*\n)*? # any number of lines, minimally matching
+ </\2> # the matching end tag
+ [ \t]* # trailing spaces/tabs
+ (?=\n+|\Z) # followed by a newline or end of document
+ )
+ """ % _block_tags_a,
+ re.X | re.M)
+
+ _block_tags_b = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math'
+ _liberal_tag_block_re = re.compile(r"""
+ ( # save in \1
+ ^ # start of line (with re.M)
+ <(%s) # start tag = \2
+ \b # word break
+ (.*\n)*? # any number of lines, minimally matching
+ .*</\2> # the matching end tag
+ [ \t]* # trailing spaces/tabs
+ (?=\n+|\Z) # followed by a newline or end of document
+ )
+ """ % _block_tags_b,
+ re.X | re.M)
+
+ def _hash_html_block_sub(self, match, raw=False):
+ html = match.group(1)
+ if raw and self.safe_mode:
+ html = self._sanitize_html(html)
+ key = _hash_text(html)
+ self.html_blocks[key] = html
+ return "\n\n" + key + "\n\n"
+
+ def _hash_html_blocks(self, text, raw=False):
+ """Hashify HTML blocks
+
+ We only want to do this for block-level HTML tags, such as headers,
+ lists, and tables. That's because we still want to wrap <p>s around
+ "paragraphs" that are wrapped in non-block-level tags, such as anchors,
+ phrase emphasis, and spans. The list of tags we're looking for is
+ hard-coded.
+
+ @param raw {boolean} indicates if these are raw HTML blocks in
+ the original source. It makes a difference in "safe" mode.
+ """
+ if '<' not in text:
+ return text
+
+ # Pass `raw` value into our calls to self._hash_html_block_sub.
+ hash_html_block_sub = _curry(self._hash_html_block_sub, raw=raw)
+
+ # First, look for nested blocks, e.g.:
+ # <div>
+ # <div>
+ # tags for inner block must be indented.
+ # </div>
+ # </div>
+ #
+ # The outermost tags must start at the left margin for this to match, and
+ # the inner nested divs must be indented.
+ # We need to do this before the next, more liberal match, because the next
+ # match will start at the first `<div>` and stop at the first `</div>`.
+ text = self._strict_tag_block_re.sub(hash_html_block_sub, text)
+
+ # Now match more liberally, simply from `\n<tag>` to `</tag>\n`
+ text = self._liberal_tag_block_re.sub(hash_html_block_sub, text)
+
+ # Special case just for <hr />. It was easier to make a special
+ # case than to make the other regex more complicated.
+ if "<hr" in text:
+ _hr_tag_re = _hr_tag_re_from_tab_width(self.tab_width)
+ text = _hr_tag_re.sub(hash_html_block_sub, text)
+
+ # Special case for standalone HTML comments:
+ if "<!--" in text:
+ start = 0
+ while True:
+ # Delimiters for next comment block.
+ try:
+ start_idx = text.index("<!--", start)
+ except ValueError, ex:
+ break
+ try:
+ end_idx = text.index("-->", start_idx) + 3
+ except ValueError, ex:
+ break
+
+ # Start position for next comment block search.
+ start = end_idx
+
+ # Validate whitespace before comment.
+ if start_idx:
+ # - Up to `tab_width - 1` spaces before start_idx.
+ for i in range(self.tab_width - 1):
+ if text[start_idx - 1] != ' ':
+ break
+ start_idx -= 1
+ if start_idx == 0:
+ break
+ # - Must be preceded by 2 newlines or hit the start of
+ # the document.
+ if start_idx == 0:
+ pass
+ elif start_idx == 1 and text[0] == '\n':
+ start_idx = 0 # to match minute detail of Markdown.pl regex
+ elif text[start_idx-2:start_idx] == '\n\n':
+ pass
+ else:
+ break
+
+ # Validate whitespace after comment.
+ # - Any number of spaces and tabs.
+ while end_idx < len(text):
+ if text[end_idx] not in ' \t':
+ break
+ end_idx += 1
+ # - Must be following by 2 newlines or hit end of text.
+ if text[end_idx:end_idx+2] not in ('', '\n', '\n\n'):
+ continue
+
+ # Escape and hash (must match `_hash_html_block_sub`).
+ html = text[start_idx:end_idx]
+ if raw and self.safe_mode:
+ html = self._sanitize_html(html)
+ key = _hash_text(html)
+ self.html_blocks[key] = html
+ text = text[:start_idx] + "\n\n" + key + "\n\n" + text[end_idx:]
+
+ if "xml" in self.extras:
+ # Treat XML processing instructions and namespaced one-liner
+ # tags as if they were block HTML tags. E.g., if standalone
+ # (i.e. are their own paragraph), the following do not get
+ # wrapped in a <p> tag:
+ # <?foo bar?>
+ #
+ # <xi:include xmlns:xi="http://www.w3.org/2001/XInclude" href="chapter_1.md"/>
+ _xml_oneliner_re = _xml_oneliner_re_from_tab_width(self.tab_width)
+ text = _xml_oneliner_re.sub(hash_html_block_sub, text)
+
+ return text
+
+ def _strip_link_definitions(self, text):
+ # Strips link definitions from text, stores the URLs and titles in
+ # hash references.
+ less_than_tab = self.tab_width - 1
+
+ # Link defs are in the form:
+ # [id]: url "optional title"
+ _link_def_re = re.compile(r"""
+ ^[ ]{0,%d}\[(.+)\]: # id = \1
+ [ \t]*
+ \n? # maybe *one* newline
+ [ \t]*
+ <?(.+?)>? # url = \2
+ [ \t]*
+ (?:
+ \n? # maybe one newline
+ [ \t]*
+ (?<=\s) # lookbehind for whitespace
+ ['"(]
+ ([^\n]*) # title = \3
+ ['")]
+ [ \t]*
+ )? # title is optional
+ (?:\n+|\Z)
+ """ % less_than_tab, re.X | re.M | re.U)
+ return _link_def_re.sub(self._extract_link_def_sub, text)
+
+ def _extract_link_def_sub(self, match):
+ id, url, title = match.groups()
+ key = id.lower() # Link IDs are case-insensitive
+ self.urls[key] = self._encode_amps_and_angles(url)
+ if title:
+ self.titles[key] = title.replace('"', '&quot;')
+ return ""
+
+ def _extract_footnote_def_sub(self, match):
+ id, text = match.groups()
+ text = _dedent(text, skip_first_line=not text.startswith('\n')).strip()
+ normed_id = re.sub(r'\W', '-', id)
+ # Ensure footnote text ends with a couple newlines (for some
+ # block gamut matches).
+ self.footnotes[normed_id] = text + "\n\n"
+ return ""
+
+ def _strip_footnote_definitions(self, text):
+ """A footnote definition looks like this:
+
+ [^note-id]: Text of the note.
+
+ May include one or more indented paragraphs.
+
+ Where,
+ - The 'note-id' can be pretty much anything, though typically it
+ is the number of the footnote.
+ - The first paragraph may start on the next line, like so:
+
+ [^note-id]:
+ Text of the note.
+ """
+ less_than_tab = self.tab_width - 1
+ footnote_def_re = re.compile(r'''
+ ^[ ]{0,%d}\[\^(.+)\]: # id = \1
+ [ \t]*
+ ( # footnote text = \2
+ # First line need not start with the spaces.
+ (?:\s*.*\n+)
+ (?:
+ (?:[ ]{%d} | \t) # Subsequent lines must be indented.
+ .*\n+
+ )*
+ )
+ # Lookahead for non-space at line-start, or end of doc.
+ (?:(?=^[ ]{0,%d}\S)|\Z)
+ ''' % (less_than_tab, self.tab_width, self.tab_width),
+ re.X | re.M)
+ return footnote_def_re.sub(self._extract_footnote_def_sub, text)
+
+
+ _hr_res = [
+ re.compile(r"^[ ]{0,2}([ ]?\*[ ]?){3,}[ \t]*$", re.M),
+ re.compile(r"^[ ]{0,2}([ ]?\-[ ]?){3,}[ \t]*$", re.M),
+ re.compile(r"^[ ]{0,2}([ ]?\_[ ]?){3,}[ \t]*$", re.M),
+ ]
+
+ def _run_block_gamut(self, text):
+ # These are all the transformations that form block-level
+ # tags like paragraphs, headers, and list items.
+
+ text = self._do_headers(text)
+
+ # Do Horizontal Rules:
+ hr = "\n<hr"+self.empty_element_suffix+"\n"
+ for hr_re in self._hr_res:
+ text = hr_re.sub(hr, text)
+
+ text = self._do_lists(text)
+
+ if "pyshell" in self.extras:
+ text = self._prepare_pyshell_blocks(text)
+
+ text = self._do_code_blocks(text)
+
+ text = self._do_block_quotes(text)
+
+ # We already ran _HashHTMLBlocks() before, in Markdown(), but that
+ # was to escape raw HTML in the original Markdown source. This time,
+ # we're escaping the markup we've just created, so that we don't wrap
+ # <p> tags around block-level tags.
+ text = self._hash_html_blocks(text)
+
+ text = self._form_paragraphs(text)
+
+ return text
+
+ def _pyshell_block_sub(self, match):
+ lines = match.group(0).splitlines(0)
+ _dedentlines(lines)
+ indent = ' ' * self.tab_width
+ s = ('\n' # separate from possible cuddled paragraph
+ + indent + ('\n'+indent).join(lines)
+ + '\n\n')
+ return s
+
+ def _prepare_pyshell_blocks(self, text):
+ """Ensure that Python interactive shell sessions are put in
+ code blocks -- even if not properly indented.
+ """
+ if ">>>" not in text:
+ return text
+
+ less_than_tab = self.tab_width - 1
+ _pyshell_block_re = re.compile(r"""
+ ^([ ]{0,%d})>>>[ ].*\n # first line
+ ^(\1.*\S+.*\n)* # any number of subsequent lines
+ ^\n # ends with a blank line
+ """ % less_than_tab, re.M | re.X)
+
+ return _pyshell_block_re.sub(self._pyshell_block_sub, text)
+
+ def _run_span_gamut(self, text):
+ # These are all the transformations that occur *within* block-level
+ # tags like paragraphs, headers, and list items.
+
+ text = self._do_code_spans(text)
+
+ text = self._escape_special_chars(text)
+
+ # Process anchor and image tags.
+ text = self._do_links(text)
+
+ # Make links out of things like `<http://example.com/>`
+ # Must come after _do_links(), because you can use < and >
+ # delimiters in inline links like [this](<url>).
+ text = self._do_auto_links(text)
+
+ if "link-patterns" in self.extras:
+ text = self._do_link_patterns(text)
+
+ text = self._encode_amps_and_angles(text)
+
+ text = self._do_italics_and_bold(text)
+
+ # Do hard breaks:
+ text = re.sub(r" {2,}\n", " <br%s\n" % self.empty_element_suffix, text)
+
+ return text
+
+ # "Sorta" because auto-links are identified as "tag" tokens.
+ _sorta_html_tokenize_re = re.compile(r"""
+ (
+ # tag
+ </?
+ (?:\w+) # tag name
+ (?:\s+(?:[\w-]+:)?[\w-]+=(?:".*?"|'.*?'))* # attributes
+ \s*/?>
+ |
+ # auto-link (e.g., <http://www.activestate.com/>)
+ <\w+[^>]*>
+ |
+ <!--.*?--> # comment
+ |
+ <\?.*?\?> # processing instruction
+ )
+ """, re.X)
+
+ def _escape_special_chars(self, text):
+ # Python markdown note: the HTML tokenization here differs from
+ # that in Markdown.pl, hence the behaviour for subtle cases can
+ # differ (I believe the tokenizer here does a better job because
+ # it isn't susceptible to unmatched '<' and '>' in HTML tags).
+ # Note, however, that '>' is not allowed in an auto-link URL
+ # here.
+ escaped = []
+ is_html_markup = False
+ for token in self._sorta_html_tokenize_re.split(text):
+ if is_html_markup:
+ # Within tags/HTML-comments/auto-links, encode * and _
+ # so they don't conflict with their use in Markdown for
+ # italics and strong. We're replacing each such
+ # character with its corresponding MD5 checksum value;
+ # this is likely overkill, but it should prevent us from
+ # colliding with the escape values by accident.
+ escaped.append(token.replace('*', g_escape_table['*'])
+ .replace('_', g_escape_table['_']))
+ else:
+ escaped.append(self._encode_backslash_escapes(token))
+ is_html_markup = not is_html_markup
+ return ''.join(escaped)
+
+ def _hash_html_spans(self, text):
+ # Used for safe_mode.
+
+ def _is_auto_link(s):
+ if ':' in s and self._auto_link_re.match(s):
+ return True
+ elif '@' in s and self._auto_email_link_re.match(s):
+ return True
+ return False
+
+ tokens = []
+ is_html_markup = False
+ for token in self._sorta_html_tokenize_re.split(text):
+ if is_html_markup and not _is_auto_link(token):
+ sanitized = self._sanitize_html(token)
+ key = _hash_text(sanitized)
+ self.html_spans[key] = sanitized
+ tokens.append(key)
+ else:
+ tokens.append(token)
+ is_html_markup = not is_html_markup
+ return ''.join(tokens)
+
+ def _unhash_html_spans(self, text):
+ for key, sanitized in self.html_spans.items():
+ text = text.replace(key, sanitized)
+ return text
+
+ def _sanitize_html(self, s):
+ if self.safe_mode == "replace":
+ return self.html_removed_text
+ elif self.safe_mode == "escape":
+ replacements = [
+ ('&', '&amp;'),
+ ('<', '&lt;'),
+ ('>', '&gt;'),
+ ]
+ for before, after in replacements:
+ s = s.replace(before, after)
+ return s
+ else:
+ raise MarkdownError("invalid value for 'safe_mode': %r (must be "
+ "'escape' or 'replace')" % self.safe_mode)
+
+ _tail_of_inline_link_re = re.compile(r'''
+ # Match tail of: [text](/url/) or [text](/url/ "title")
+ \( # literal paren
+ [ \t]*
+ (?P<url> # \1
+ <.*?>
+ |
+ .*?
+ )
+ [ \t]*
+ ( # \2
+ (['"]) # quote char = \3
+ (?P<title>.*?)
+ \3 # matching quote
+ )? # title is optional
+ \)
+ ''', re.X | re.S)
+ _tail_of_reference_link_re = re.compile(r'''
+ # Match tail of: [text][id]
+ [ ]? # one optional space
+ (?:\n[ ]*)? # one optional newline followed by spaces
+ \[
+ (?P<id>.*?)
+ \]
+ ''', re.X | re.S)
+
+ def _do_links(self, text):
+ """Turn Markdown link shortcuts into XHTML <a> and <img> tags.
+
+ This is a combination of Markdown.pl's _DoAnchors() and
+ _DoImages(). They are done together because that simplified the
+ approach. It was necessary to use a different approach than
+ Markdown.pl because of the lack of atomic matching support in
+ Python's regex engine used in $g_nested_brackets.
+ """
+ MAX_LINK_TEXT_SENTINEL = 3000 # markdown2 issue 24
+
+ # `anchor_allowed_pos` is used to support img links inside
+ # anchors, but not anchors inside anchors. An anchor's start
+ # pos must be `>= anchor_allowed_pos`.
+ anchor_allowed_pos = 0
+
+ curr_pos = 0
+ while True: # Handle the next link.
+ # The next '[' is the start of:
+ # - an inline anchor: [text](url "title")
+ # - a reference anchor: [text][id]
+ # - an inline img: ![text](url "title")
+ # - a reference img: ![text][id]
+ # - a footnote ref: [^id]
+ # (Only if 'footnotes' extra enabled)
+ # - a footnote defn: [^id]: ...
+ # (Only if 'footnotes' extra enabled) These have already
+ # been stripped in _strip_footnote_definitions() so no
+ # need to watch for them.
+ # - a link definition: [id]: url "title"
+ # These have already been stripped in
+ # _strip_link_definitions() so no need to watch for them.
+ # - not markup: [...anything else...
+ try:
+ start_idx = text.index('[', curr_pos)
+ except ValueError:
+ break
+ text_length = len(text)
+
+ # Find the matching closing ']'.
+ # Markdown.pl allows *matching* brackets in link text so we
+ # will here too. Markdown.pl *doesn't* currently allow
+ # matching brackets in img alt text -- we'll differ in that
+ # regard.
+ bracket_depth = 0
+ for p in range(start_idx+1, min(start_idx+MAX_LINK_TEXT_SENTINEL,
+ text_length)):
+ ch = text[p]
+ if ch == ']':
+ bracket_depth -= 1
+ if bracket_depth < 0:
+ break
+ elif ch == '[':
+ bracket_depth += 1
+ else:
+ # Closing bracket not found within sentinel length.
+ # This isn't markup.
+ curr_pos = start_idx + 1
+ continue
+ link_text = text[start_idx+1:p]
+
+ # Possibly a footnote ref?
+ if "footnotes" in self.extras and link_text.startswith("^"):
+ normed_id = re.sub(r'\W', '-', link_text[1:])
+ if normed_id in self.footnotes:
+ self.footnote_ids.append(normed_id)
+ result = '<sup class="footnote-ref" id="fnref-%s">' \
+ '<a href="#fn-%s">%s</a></sup>' \
+ % (normed_id, normed_id, len(self.footnote_ids))
+ text = text[:start_idx] + result + text[p+1:]
+ else:
+ # This id isn't defined, leave the markup alone.
+ curr_pos = p+1
+ continue
+
+ # Now determine what this is by the remainder.
+ p += 1
+ if p == text_length:
+ return text
+
+ # Inline anchor or img?
+ if text[p] == '(': # attempt at perf improvement
+ match = self._tail_of_inline_link_re.match(text, p)
+ if match:
+ # Handle an inline anchor or img.
+ is_img = start_idx > 0 and text[start_idx-1] == "!"
+ if is_img:
+ start_idx -= 1
+
+ url, title = match.group("url"), match.group("title")
+ if url and url[0] == '<':
+ url = url[1:-1] # '<url>' -> 'url'
+ # We've got to encode these to avoid conflicting
+ # with italics/bold.
+ url = url.replace('*', g_escape_table['*']) \
+ .replace('_', g_escape_table['_'])
+ if title:
+ title_str = ' title="%s"' \
+ % title.replace('*', g_escape_table['*']) \
+ .replace('_', g_escape_table['_']) \
+ .replace('"', '&quot;')
+ else:
+ title_str = ''
+ if is_img:
+ result = '<img src="%s" alt="%s"%s%s' \
+ % (url, link_text.replace('"', '&quot;'),
+ title_str, self.empty_element_suffix)
+ curr_pos = start_idx + len(result)
+ text = text[:start_idx] + result + text[match.end():]
+ elif start_idx >= anchor_allowed_pos:
+ result_head = '<a href="%s"%s>' % (url, title_str)
+ result = '%s%s</a>' % (result_head, link_text)
+ # <img> allowed from curr_pos on, <a> from
+ # anchor_allowed_pos on.
+ curr_pos = start_idx + len(result_head)
+ anchor_allowed_pos = start_idx + len(result)
+ text = text[:start_idx] + result + text[match.end():]
+ else:
+ # Anchor not allowed here.
+ curr_pos = start_idx + 1
+ continue
+
+ # Reference anchor or img?
+ else:
+ match = self._tail_of_reference_link_re.match(text, p)
+ if match:
+ # Handle a reference-style anchor or img.
+ is_img = start_idx > 0 and text[start_idx-1] == "!"
+ if is_img:
+ start_idx -= 1
+ link_id = match.group("id").lower()
+ if not link_id:
+ link_id = link_text.lower() # for links like [this][]
+ if link_id in self.urls:
+ url = self.urls[link_id]
+ # We've got to encode these to avoid conflicting
+ # with italics/bold.
+ url = url.replace('*', g_escape_table['*']) \
+ .replace('_', g_escape_table['_'])
+ title = self.titles.get(link_id)
+ if title:
+ title = title.replace('*', g_escape_table['*']) \
+ .replace('_', g_escape_table['_'])
+ title_str = ' title="%s"' % title
+ else:
+ title_str = ''
+ if is_img:
+ result = '<img src="%s" alt="%s"%s%s' \
+ % (url, link_text.replace('"', '&quot;'),
+ title_str, self.empty_element_suffix)
+ curr_pos = start_idx + len(result)
+ text = text[:start_idx] + result + text[match.end():]
+ elif start_idx >= anchor_allowed_pos:
+ result = '<a href="%s"%s>%s</a>' \
+ % (url, title_str, link_text)
+ result_head = '<a href="%s"%s>' % (url, title_str)
+ result = '%s%s</a>' % (result_head, link_text)
+ # <img> allowed from curr_pos on, <a> from
+ # anchor_allowed_pos on.
+ curr_pos = start_idx + len(result_head)
+ anchor_allowed_pos = start_idx + len(result)
+ text = text[:start_idx] + result + text[match.end():]
+ else:
+ # Anchor not allowed here.
+ curr_pos = start_idx + 1
+ else:
+ # This id isn't defined, leave the markup alone.
+ curr_pos = match.end()
+ continue
+
+ # Otherwise, it isn't markup.
+ curr_pos = start_idx + 1
+
+ return text
+
+
+ _setext_h_re = re.compile(r'^(.+)[ \t]*\n(=+|-+)[ \t]*\n+', re.M)
+ def _setext_h_sub(self, match):
+ n = {"=": 1, "-": 2}[match.group(2)[0]]
+ demote_headers = self.extras.get("demote-headers")
+ if demote_headers:
+ n = min(n + demote_headers, 6)
+ return "<h%d>%s</h%d>\n\n" \
+ % (n, self._run_span_gamut(match.group(1)), n)
+
+ _atx_h_re = re.compile(r'''
+ ^(\#{1,6}) # \1 = string of #'s
+ [ \t]*
+ (.+?) # \2 = Header text
+ [ \t]*
+ (?<!\\) # ensure not an escaped trailing '#'
+ \#* # optional closing #'s (not counted)
+ \n+
+ ''', re.X | re.M)
+ def _atx_h_sub(self, match):
+ n = len(match.group(1))
+ demote_headers = self.extras.get("demote-headers")
+ if demote_headers:
+ n = min(n + demote_headers, 6)
+ return "<h%d>%s</h%d>\n\n" \
+ % (n, self._run_span_gamut(match.group(2)), n)
+
+ def _do_headers(self, text):
+ # Setext-style headers:
+ # Header 1
+ # ========
+ #
+ # Header 2
+ # --------
+ text = self._setext_h_re.sub(self._setext_h_sub, text)
+
+ # atx-style headers:
+ # # Header 1
+ # ## Header 2
+ # ## Header 2 with closing hashes ##
+ # ...
+ # ###### Header 6
+ text = self._atx_h_re.sub(self._atx_h_sub, text)
+
+ return text
+
+
+ _marker_ul_chars = '*+-'
+ _marker_any = r'(?:[%s]|\d+\.)' % _marker_ul_chars
+ _marker_ul = '(?:[%s])' % _marker_ul_chars
+ _marker_ol = r'(?:\d+\.)'
+
+ def _list_sub(self, match):
+ lst = match.group(1)
+ lst_type = match.group(3) in self._marker_ul_chars and "ul" or "ol"
+ result = self._process_list_items(lst)
+ if self.list_level:
+ return "<%s>\n%s</%s>\n" % (lst_type, result, lst_type)
+ else:
+ return "<%s>\n%s</%s>\n\n" % (lst_type, result, lst_type)
+
+ def _do_lists(self, text):
+ # Form HTML ordered (numbered) and unordered (bulleted) lists.
+
+ for marker_pat in (self._marker_ul, self._marker_ol):
+ # Re-usable pattern to match any entire ul or ol list:
+ less_than_tab = self.tab_width - 1
+ whole_list = r'''
+ ( # \1 = whole list
+ ( # \2
+ [ ]{0,%d}
+ (%s) # \3 = first list item marker
+ [ \t]+
+ )
+ (?:.+?)
+ ( # \4
+ \Z
+ |
+ \n{2,}
+ (?=\S)
+ (?! # Negative lookahead for another list item marker
+ [ \t]*
+ %s[ \t]+
+ )
+ )
+ )
+ ''' % (less_than_tab, marker_pat, marker_pat)
+
+ # We use a different prefix before nested lists than top-level lists.
+ # See extended comment in _process_list_items().
+ #
+ # Note: There's a bit of duplication here. My original implementation
+ # created a scalar regex pattern as the conditional result of the test on
+ # $g_list_level, and then only ran the $text =~ s{...}{...}egmx
+ # substitution once, using the scalar as the pattern. This worked,
+ # everywhere except when running under MT on my hosting account at Pair
+ # Networks. There, this caused all rebuilds to be killed by the reaper (or
+ # perhaps they crashed, but that seems incredibly unlikely given that the
+ # same script on the same server ran fine *except* under MT. I've spent
+ # more time trying to figure out why this is happening than I'd like to
+ # admit. My only guess, backed up by the fact that this workaround works,
+ # is that Perl optimizes the substition when it can figure out that the
+ # pattern will never change, and when this optimization isn't on, we run
+ # afoul of the reaper. Thus, the slightly redundant code to that uses two
+ # static s/// patterns rather than one conditional pattern.
+
+ if self.list_level:
+ sub_list_re = re.compile("^"+whole_list, re.X | re.M | re.S)
+ text = sub_list_re.sub(self._list_sub, text)
+ else:
+ list_re = re.compile(r"(?:(?<=\n\n)|\A\n?)"+whole_list,
+ re.X | re.M | re.S)
+ text = list_re.sub(self._list_sub, text)
+
+ return text
+
+ _list_item_re = re.compile(r'''
+ (\n)? # leading line = \1
+ (^[ \t]*) # leading whitespace = \2
+ (%s) [ \t]+ # list marker = \3
+ ((?:.+?) # list item text = \4
+ (\n{1,2})) # eols = \5
+ (?= \n* (\Z | \2 (%s) [ \t]+))
+ ''' % (_marker_any, _marker_any),
+ re.M | re.X | re.S)
+
+ _last_li_endswith_two_eols = False
+ def _list_item_sub(self, match):
+ item = match.group(4)
+ leading_line = match.group(1)
+ leading_space = match.group(2)
+ if leading_line or "\n\n" in item or self._last_li_endswith_two_eols:
+ item = self._run_block_gamut(self._outdent(item))
+ else:
+ # Recursion for sub-lists:
+ item = self._do_lists(self._outdent(item))
+ if item.endswith('\n'):
+ item = item[:-1]
+ item = self._run_span_gamut(item)
+ self._last_li_endswith_two_eols = (len(match.group(5)) == 2)
+ return "<li>%s</li>\n" % item
+
+ def _process_list_items(self, list_str):
+ # Process the contents of a single ordered or unordered list,
+ # splitting it into individual list items.
+
+ # The $g_list_level global keeps track of when we're inside a list.
+ # Each time we enter a list, we increment it; when we leave a list,
+ # we decrement. If it's zero, we're not in a list anymore.
+ #
+ # We do this because when we're not inside a list, we want to treat
+ # something like this:
+ #
+ # I recommend upgrading to version
+ # 8. Oops, now this line is treated
+ # as a sub-list.
+ #
+ # As a single paragraph, despite the fact that the second line starts
+ # with a digit-period-space sequence.
+ #
+ # Whereas when we're inside a list (or sub-list), that line will be
+ # treated as the start of a sub-list. What a kludge, huh? This is
+ # an aspect of Markdown's syntax that's hard to parse perfectly
+ # without resorting to mind-reading. Perhaps the solution is to
+ # change the syntax rules such that sub-lists must start with a
+ # starting cardinal number; e.g. "1." or "a.".
+ self.list_level += 1
+ self._last_li_endswith_two_eols = False
+ list_str = list_str.rstrip('\n') + '\n'
+ list_str = self._list_item_re.sub(self._list_item_sub, list_str)
+ self.list_level -= 1
+ return list_str
+
+ def _get_pygments_lexer(self, lexer_name):
+ try:
+ from pygments import lexers, util
+ except ImportError:
+ return None
+ try:
+ return lexers.get_lexer_by_name(lexer_name)
+ except util.ClassNotFound:
+ return None
+
+ def _color_with_pygments(self, codeblock, lexer, **formatter_opts):
+ import pygments
+ import pygments.formatters
+
+ class HtmlCodeFormatter(pygments.formatters.HtmlFormatter):
+ def _wrap_code(self, inner):
+ """A function for use in a Pygments Formatter which
+ wraps in <code> tags.
+ """
+ yield 0, "<code>"
+ for tup in inner:
+ yield tup
+ yield 0, "</code>"
+
+ def wrap(self, source, outfile):
+ """Return the source with a code, pre, and div."""
+ return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
+
+ formatter = HtmlCodeFormatter(cssclass="codehilite", **formatter_opts)
+ return pygments.highlight(codeblock, lexer, formatter)
+
+ def _code_block_sub(self, match):
+ codeblock = match.group(1)
+ codeblock = self._outdent(codeblock)
+ codeblock = self._detab(codeblock)
+ codeblock = codeblock.lstrip('\n') # trim leading newlines
+ codeblock = codeblock.rstrip() # trim trailing whitespace
+
+ if "code-color" in self.extras and codeblock.startswith(":::"):
+ lexer_name, rest = codeblock.split('\n', 1)
+ lexer_name = lexer_name[3:].strip()
+ lexer = self._get_pygments_lexer(lexer_name)
+ codeblock = rest.lstrip("\n") # Remove lexer declaration line.
+ if lexer:
+ formatter_opts = self.extras['code-color'] or {}
+ colored = self._color_with_pygments(codeblock, lexer,
+ **formatter_opts)
+ return "\n\n%s\n\n" % colored
+
+ codeblock = self._encode_code(codeblock)
+ return "\n\n<pre><code>%s\n</code></pre>\n\n" % codeblock
+
+ def _do_code_blocks(self, text):
+ """Process Markdown `<pre><code>` blocks."""
+ code_block_re = re.compile(r'''
+ (?:\n\n|\A)
+ ( # $1 = the code block -- one or more lines, starting with a space/tab
+ (?:
+ (?:[ ]{%d} | \t) # Lines must start with a tab or a tab-width of spaces
+ .*\n+
+ )+
+ )
+ ((?=^[ ]{0,%d}\S)|\Z) # Lookahead for non-space at line-start, or end of doc
+ ''' % (self.tab_width, self.tab_width),
+ re.M | re.X)
+
+ return code_block_re.sub(self._code_block_sub, text)
+
+
+ # Rules for a code span:
+ # - backslash escapes are not interpreted in a code span
+ # - to include one or or a run of more backticks the delimiters must
+ # be a longer run of backticks
+ # - cannot start or end a code span with a backtick; pad with a
+ # space and that space will be removed in the emitted HTML
+ # See `test/tm-cases/escapes.text` for a number of edge-case
+ # examples.
+ _code_span_re = re.compile(r'''
+ (?<!\\)
+ (`+) # \1 = Opening run of `
+ (?!`) # See Note A test/tm-cases/escapes.text
+ (.+?) # \2 = The code block
+ (?<!`)
+ \1 # Matching closer
+ (?!`)
+ ''', re.X | re.S)
+
+ def _code_span_sub(self, match):
+ c = match.group(2).strip(" \t")
+ c = self._encode_code(c)
+ return "<code>%s</code>" % c
+
+ def _do_code_spans(self, text):
+ # * Backtick quotes are used for <code></code> spans.
+ #
+ # * You can use multiple backticks as the delimiters if you want to
+ # include literal backticks in the code span. So, this input:
+ #
+ # Just type ``foo `bar` baz`` at the prompt.
+ #
+ # Will translate to:
+ #
+ # <p>Just type <code>foo `bar` baz</code> at the prompt.</p>
+ #
+ # There's no arbitrary limit to the number of backticks you
+ # can use as delimters. If you need three consecutive backticks
+ # in your code, use four for delimiters, etc.
+ #
+ # * You can use spaces to get literal backticks at the edges:
+ #
+ # ... type `` `bar` `` ...
+ #
+ # Turns to:
+ #
+ # ... type <code>`bar`</code> ...
+ return self._code_span_re.sub(self._code_span_sub, text)
+
+ def _encode_code(self, text):
+ """Encode/escape certain characters inside Markdown code runs.
+ The point is that in code, these characters are literals,
+ and lose their special Markdown meanings.
+ """
+ replacements = [
+ # Encode all ampersands; HTML entities are not
+ # entities within a Markdown code span.
+ ('&', '&amp;'),
+ # Do the angle bracket song and dance:
+ ('<', '&lt;'),
+ ('>', '&gt;'),
+ # Now, escape characters that are magic in Markdown:
+ ('*', g_escape_table['*']),
+ ('_', g_escape_table['_']),
+ ('{', g_escape_table['{']),
+ ('}', g_escape_table['}']),
+ ('[', g_escape_table['[']),
+ (']', g_escape_table[']']),
+ ('\\', g_escape_table['\\']),
+ ]
+ for before, after in replacements:
+ text = text.replace(before, after)
+ return text
+
+ _strong_re = re.compile(r"(\*\*|__)(?=\S)(.+?[*_]*)(?<=\S)\1", re.S)
+ _em_re = re.compile(r"(\*|_)(?=\S)(.+?)(?<=\S)\1", re.S)
+ _code_friendly_strong_re = re.compile(r"\*\*(?=\S)(.+?[*_]*)(?<=\S)\*\*", re.S)
+ _code_friendly_em_re = re.compile(r"\*(?=\S)(.+?)(?<=\S)\*", re.S)
+ def _do_italics_and_bold(self, text):
+ # <strong> must go first:
+ if "code-friendly" in self.extras:
+ text = self._code_friendly_strong_re.sub(r"<strong>\1</strong>", text)
+ text = self._code_friendly_em_re.sub(r"<em>\1</em>", text)
+ else:
+ text = self._strong_re.sub(r"<strong>\2</strong>", text)
+ text = self._em_re.sub(r"<em>\2</em>", text)
+ return text
+
+
+ _block_quote_re = re.compile(r'''
+ ( # Wrap whole match in \1
+ (
+ ^[ \t]*>[ \t]? # '>' at the start of a line
+ .+\n # rest of the first line
+ (.+\n)* # subsequent consecutive lines
+ \n* # blanks
+ )+
+ )
+ ''', re.M | re.X)
+ _bq_one_level_re = re.compile('^[ \t]*>[ \t]?', re.M);
+
+ _html_pre_block_re = re.compile(r'(\s*<pre>.+?</pre>)', re.S)
+ def _dedent_two_spaces_sub(self, match):
+ return re.sub(r'(?m)^ ', '', match.group(1))
+
+ def _block_quote_sub(self, match):
+ bq = match.group(1)
+ bq = self._bq_one_level_re.sub('', bq) # trim one level of quoting
+ bq = self._ws_only_line_re.sub('', bq) # trim whitespace-only lines
+ bq = self._run_block_gamut(bq) # recurse
+
+ bq = re.sub('(?m)^', ' ', bq)
+ # These leading spaces screw with <pre> content, so we need to fix that:
+ bq = self._html_pre_block_re.sub(self._dedent_two_spaces_sub, bq)
+
+ return "<blockquote>\n%s\n</blockquote>\n\n" % bq
+
+ def _do_block_quotes(self, text):
+ if '>' not in text:
+ return text
+ return self._block_quote_re.sub(self._block_quote_sub, text)
+
+ def _form_paragraphs(self, text):
+ # Strip leading and trailing lines:
+ text = text.strip('\n')
+
+ # Wrap <p> tags.
+ grafs = re.split(r"\n{2,}", text)
+ for i, graf in enumerate(grafs):
+ if graf in self.html_blocks:
+ # Unhashify HTML blocks
+ grafs[i] = self.html_blocks[graf]
+ else:
+ # Wrap <p> tags.
+ graf = self._run_span_gamut(graf)
+ grafs[i] = "<p>" + graf.lstrip(" \t") + "</p>"
+
+ return "\n\n".join(grafs)
+
+ def _add_footnotes(self, text):
+ if self.footnotes:
+ footer = [
+ '<div class="footnotes">',
+ '<hr' + self.empty_element_suffix,
+ '<ol>',
+ ]
+ for i, id in enumerate(self.footnote_ids):
+ if i != 0:
+ footer.append('')
+ footer.append('<li id="fn-%s">' % id)
+ footer.append(self._run_block_gamut(self.footnotes[id]))
+ backlink = ('<a href="#fnref-%s" '
+ 'class="footnoteBackLink" '
+ 'title="Jump back to footnote %d in the text.">'
+ '&#8617;</a>' % (id, i+1))
+ if footer[-1].endswith("</p>"):
+ footer[-1] = footer[-1][:-len("</p>")] \
+ + '&nbsp;' + backlink + "</p>"
+ else:
+ footer.append("\n<p>%s</p>" % backlink)
+ footer.append('</li>')
+ footer.append('</ol>')
+ footer.append('</div>')
+ return text + '\n\n' + '\n'.join(footer)
+ else:
+ return text
+
+ # Ampersand-encoding based entirely on Nat Irons's Amputator MT plugin:
+ # http://bumppo.net/projects/amputator/
+ _ampersand_re = re.compile(r'&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)')
+ _naked_lt_re = re.compile(r'<(?![a-z/?\$!])', re.I)
+ _naked_gt_re = re.compile(r'''(?<![a-z?!/'"-])>''', re.I)
+
+ def _encode_amps_and_angles(self, text):
+ # Smart processing for ampersands and angle brackets that need
+ # to be encoded.
+ text = self._ampersand_re.sub('&amp;', text)
+
+ # Encode naked <'s
+ text = self._naked_lt_re.sub('&lt;', text)
+
+ # Encode naked >'s
+ # Note: Other markdown implementations (e.g. Markdown.pl, PHP
+ # Markdown) don't do this.
+ text = self._naked_gt_re.sub('&gt;', text)
+ return text
+
+ def _encode_backslash_escapes(self, text):
+ for ch, escape in g_escape_table.items():
+ text = text.replace("\\"+ch, escape)
+ return text
+
+ _auto_link_re = re.compile(r'<((https?|ftp):[^\'">\s]+)>', re.I)
+ def _auto_link_sub(self, match):
+ g1 = match.group(1)
+ return '<a href="%s">%s</a>' % (g1, g1)
+
+ _auto_email_link_re = re.compile(r"""
+ <
+ (?:mailto:)?
+ (
+ [-.\w]+
+ \@
+ [-\w]+(\.[-\w]+)*\.[a-z]+
+ )
+ >
+ """, re.I | re.X | re.U)
+ def _auto_email_link_sub(self, match):
+ return self._encode_email_address(
+ self._unescape_special_chars(match.group(1)))
+
+ def _do_auto_links(self, text):
+ text = self._auto_link_re.sub(self._auto_link_sub, text)
+ text = self._auto_email_link_re.sub(self._auto_email_link_sub, text)
+ return text
+
+ def _encode_email_address(self, addr):
+ # Input: an email address, e.g. "foo@example.com"
+ #
+ # Output: the email address as a mailto link, with each character
+ # of the address encoded as either a decimal or hex entity, in
+ # the hopes of foiling most address harvesting spam bots. E.g.:
+ #
+ # <a href="&#x6D;&#97;&#105;&#108;&#x74;&#111;:&#102;&#111;&#111;&#64;&#101;
+ # x&#x61;&#109;&#x70;&#108;&#x65;&#x2E;&#99;&#111;&#109;">&#102;&#111;&#111;
+ # &#64;&#101;x&#x61;&#109;&#x70;&#108;&#x65;&#x2E;&#99;&#111;&#109;</a>
+ #
+ # Based on a filter by Matthew Wickline, posted to the BBEdit-Talk
+ # mailing list: <http://tinyurl.com/yu7ue>
+ chars = [_xml_encode_email_char_at_random(ch)
+ for ch in "mailto:" + addr]
+ # Strip the mailto: from the visible part.
+ addr = '<a href="%s">%s</a>' \
+ % (''.join(chars), ''.join(chars[7:]))
+ return addr
+
+ def _do_link_patterns(self, text):
+ """Caveat emptor: there isn't much guarding against link
+ patterns being formed inside other standard Markdown links, e.g.
+ inside a [link def][like this].
+
+ Dev Notes: *Could* consider prefixing regexes with a negative
+ lookbehind assertion to attempt to guard against this.
+ """
+ link_from_hash = {}
+ for regex, repl in self.link_patterns:
+ replacements = []
+ for match in regex.finditer(text):
+ if hasattr(repl, "__call__"):
+ href = repl(match)
+ else:
+ href = match.expand(repl)
+ replacements.append((match.span(), href))
+ for (start, end), href in reversed(replacements):
+ escaped_href = (
+ href.replace('"', '&quot;') # b/c of attr quote
+ # To avoid markdown <em> and <strong>:
+ .replace('*', g_escape_table['*'])
+ .replace('_', g_escape_table['_']))
+ link = '<a href="%s">%s</a>' % (escaped_href, text[start:end])
+ hash = md5(link).hexdigest()
+ link_from_hash[hash] = link
+ text = text[:start] + hash + text[end:]
+ for hash, link in link_from_hash.items():
+ text = text.replace(hash, link)
+ return text
+
+ def _unescape_special_chars(self, text):
+ # Swap back in all the special characters we've hidden.
+ for ch, hash in g_escape_table.items():
+ text = text.replace(hash, ch)
+ return text
+
+ def _outdent(self, text):
+ # Remove one level of line-leading tabs or spaces
+ return self._outdent_re.sub('', text)
+
+
+class MarkdownWithExtras(Markdown):
+ """A markdowner class that enables most extras:
+
+ - footnotes
+ - code-color (only has effect if 'pygments' Python module on path)
+
+ These are not included:
+ - pyshell (specific to Python-related documenting)
+ - code-friendly (because it *disables* part of the syntax)
+ - link-patterns (because you need to specify some actual
+ link-patterns anyway)
+ """
+ extras = ["footnotes", "code-color"]
+
+
+#---- internal support functions
+
+# From http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52549
+def _curry(*args, **kwargs):
+ function, args = args[0], args[1:]
+ def result(*rest, **kwrest):
+ combined = kwargs.copy()
+ combined.update(kwrest)
+ return function(*args + rest, **combined)
+ return result
+
+# Recipe: regex_from_encoded_pattern (1.0)
+def _regex_from_encoded_pattern(s):
+ """'foo' -> re.compile(re.escape('foo'))
+ '/foo/' -> re.compile('foo')
+ '/foo/i' -> re.compile('foo', re.I)
+ """
+ if s.startswith('/') and s.rfind('/') != 0:
+ # Parse it: /PATTERN/FLAGS
+ idx = s.rfind('/')
+ pattern, flags_str = s[1:idx], s[idx+1:]
+ flag_from_char = {
+ "i": re.IGNORECASE,
+ "l": re.LOCALE,
+ "s": re.DOTALL,
+ "m": re.MULTILINE,
+ "u": re.UNICODE,
+ }
+ flags = 0
+ for char in flags_str:
+ try:
+ flags |= flag_from_char[char]
+ except KeyError:
+ raise ValueError("unsupported regex flag: '%s' in '%s' "
+ "(must be one of '%s')"
+ % (char, s, ''.join(flag_from_char.keys())))
+ return re.compile(s[1:idx], flags)
+ else: # not an encoded regex
+ return re.compile(re.escape(s))
+
+# Recipe: dedent (0.1.2)
+def _dedentlines(lines, tabsize=8, skip_first_line=False):
+ """_dedentlines(lines, tabsize=8, skip_first_line=False) -> dedented lines
+
+ "lines" is a list of lines to dedent.
+ "tabsize" is the tab width to use for indent width calculations.
+ "skip_first_line" is a boolean indicating if the first line should
+ be skipped for calculating the indent width and for dedenting.
+ This is sometimes useful for docstrings and similar.
+
+ Same as dedent() except operates on a sequence of lines. Note: the
+ lines list is modified **in-place**.
+ """
+ DEBUG = False
+ if DEBUG:
+ print "dedent: dedent(..., tabsize=%d, skip_first_line=%r)"\
+ % (tabsize, skip_first_line)
+ indents = []
+ margin = None
+ for i, line in enumerate(lines):
+ if i == 0 and skip_first_line: continue
+ indent = 0
+ for ch in line:
+ if ch == ' ':
+ indent += 1
+ elif ch == '\t':
+ indent += tabsize - (indent % tabsize)
+ elif ch in '\r\n':
+ continue # skip all-whitespace lines
+ else:
+ break
+ else:
+ continue # skip all-whitespace lines
+ if DEBUG: print "dedent: indent=%d: %r" % (indent, line)
+ if margin is None:
+ margin = indent
+ else:
+ margin = min(margin, indent)
+ if DEBUG: print "dedent: margin=%r" % margin
+
+ if margin is not None and margin > 0:
+ for i, line in enumerate(lines):
+ if i == 0 and skip_first_line: continue
+ removed = 0
+ for j, ch in enumerate(line):
+ if ch == ' ':
+ removed += 1
+ elif ch == '\t':
+ removed += tabsize - (removed % tabsize)
+ elif ch in '\r\n':
+ if DEBUG: print "dedent: %r: EOL -> strip up to EOL" % line
+ lines[i] = lines[i][j:]
+ break
+ else:
+ raise ValueError("unexpected non-whitespace char %r in "
+ "line %r while removing %d-space margin"
+ % (ch, line, margin))
+ if DEBUG:
+ print "dedent: %r: %r -> removed %d/%d"\
+ % (line, ch, removed, margin)
+ if removed == margin:
+ lines[i] = lines[i][j+1:]
+ break
+ elif removed > margin:
+ lines[i] = ' '*(removed-margin) + lines[i][j+1:]
+ break
+ else:
+ if removed:
+ lines[i] = lines[i][removed:]
+ return lines
+
+def _dedent(text, tabsize=8, skip_first_line=False):
+ """_dedent(text, tabsize=8, skip_first_line=False) -> dedented text
+
+ "text" is the text to dedent.
+ "tabsize" is the tab width to use for indent width calculations.
+ "skip_first_line" is a boolean indicating if the first line should
+ be skipped for calculating the indent width and for dedenting.
+ This is sometimes useful for docstrings and similar.
+
+ textwrap.dedent(s), but don't expand tabs to spaces
+ """
+ lines = text.splitlines(1)
+ _dedentlines(lines, tabsize=tabsize, skip_first_line=skip_first_line)
+ return ''.join(lines)
+
+
+class _memoized(object):
+ """Decorator that caches a function's return value each time it is called.
+ If called later with the same arguments, the cached value is returned, and
+ not re-evaluated.
+
+ http://wiki.python.org/moin/PythonDecoratorLibrary
+ """
+ def __init__(self, func):
+ self.func = func
+ self.cache = {}
+ def __call__(self, *args):
+ try:
+ return self.cache[args]
+ except KeyError:
+ self.cache[args] = value = self.func(*args)
+ return value
+ except TypeError:
+ # uncachable -- for instance, passing a list as an argument.
+ # Better to not cache than to blow up entirely.
+ return self.func(*args)
+ def __repr__(self):
+ """Return the function's docstring."""
+ return self.func.__doc__
+
+
+def _xml_oneliner_re_from_tab_width(tab_width):
+ """Standalone XML processing instruction regex."""
+ return re.compile(r"""
+ (?:
+ (?<=\n\n) # Starting after a blank line
+ | # or
+ \A\n? # the beginning of the doc
+ )
+ ( # save in $1
+ [ ]{0,%d}
+ (?:
+ <\?\w+\b\s+.*?\?> # XML processing instruction
+ |
+ <\w+:\w+\b\s+.*?/> # namespaced single tag
+ )
+ [ \t]*
+ (?=\n{2,}|\Z) # followed by a blank line or end of document
+ )
+ """ % (tab_width - 1), re.X)
+_xml_oneliner_re_from_tab_width = _memoized(_xml_oneliner_re_from_tab_width)
+
+def _hr_tag_re_from_tab_width(tab_width):
+ return re.compile(r"""
+ (?:
+ (?<=\n\n) # Starting after a blank line
+ | # or
+ \A\n? # the beginning of the doc
+ )
+ ( # save in \1
+ [ ]{0,%d}
+ <(hr) # start tag = \2
+ \b # word break
+ ([^<>])*? #
+ /?> # the matching end tag
+ [ \t]*
+ (?=\n{2,}|\Z) # followed by a blank line or end of document
+ )
+ """ % (tab_width - 1), re.X)
+_hr_tag_re_from_tab_width = _memoized(_hr_tag_re_from_tab_width)
+
+
+def _xml_encode_email_char_at_random(ch):
+ r = random()
+ # Roughly 10% raw, 45% hex, 45% dec.
+ # '@' *must* be encoded. I [John Gruber] insist.
+ # Issue 26: '_' must be encoded.
+ if r > 0.9 and ch not in "@_":
+ return ch
+ elif r < 0.45:
+ # The [1:] is to drop leading '0': 0x63 -> x63
+ return '&#%s;' % hex(ord(ch))[1:]
+ else:
+ return '&#%s;' % ord(ch)
+
+def _hash_text(text):
+ return 'md5:'+md5(text.encode("utf-8")).hexdigest()
+
+
+#---- mainline
+
+class _NoReflowFormatter(optparse.IndentedHelpFormatter):
+ """An optparse formatter that does NOT reflow the description."""
+ def format_description(self, description):
+ return description or ""
+
+def _test():
+ import doctest
+ doctest.testmod()
+
+def main(argv=None):
+ if argv is None:
+ argv = sys.argv
+ if not logging.root.handlers:
+ logging.basicConfig()
+
+ usage = "usage: %prog [PATHS...]"
+ version = "%prog "+__version__
+ parser = optparse.OptionParser(prog="markdown2", usage=usage,
+ version=version, description=cmdln_desc,
+ formatter=_NoReflowFormatter())
+ parser.add_option("-v", "--verbose", dest="log_level",
+ action="store_const", const=logging.DEBUG,
+ help="more verbose output")
+ parser.add_option("--encoding",
+ help="specify encoding of text content")
+ parser.add_option("--html4tags", action="store_true", default=False,
+ help="use HTML 4 style for empty element tags")
+ parser.add_option("-s", "--safe", metavar="MODE", dest="safe_mode",
+ help="sanitize literal HTML: 'escape' escapes "
+ "HTML meta chars, 'replace' replaces with an "
+ "[HTML_REMOVED] note")
+ parser.add_option("-x", "--extras", action="append",
+ help="Turn on specific extra features (not part of "
+ "the core Markdown spec). Supported values: "
+ "'code-friendly' disables _/__ for emphasis; "
+ "'code-color' adds code-block syntax coloring; "
+ "'link-patterns' adds auto-linking based on patterns; "
+ "'footnotes' adds the footnotes syntax;"
+ "'xml' passes one-liner processing instructions and namespaced XML tags;"
+ "'pyshell' to put unindented Python interactive shell sessions in a <code> block.")
+ parser.add_option("--use-file-vars",
+ help="Look for and use Emacs-style 'markdown-extras' "
+ "file var to turn on extras. See "
+ "<http://code.google.com/p/python-markdown2/wiki/Extras>.")
+ parser.add_option("--link-patterns-file",
+ help="path to a link pattern file")
+ parser.add_option("--self-test", action="store_true",
+ help="run internal self-tests (some doctests)")
+ parser.add_option("--compare", action="store_true",
+ help="run against Markdown.pl as well (for testing)")
+ parser.set_defaults(log_level=logging.INFO, compare=False,
+ encoding="utf-8", safe_mode=None, use_file_vars=False)
+ opts, paths = parser.parse_args()
+ log.setLevel(opts.log_level)
+
+ if opts.self_test:
+ return _test()
+
+ if opts.extras:
+ extras = {}
+ for s in opts.extras:
+ splitter = re.compile("[,;: ]+")
+ for e in splitter.split(s):
+ if '=' in e:
+ ename, earg = e.split('=', 1)
+ try:
+ earg = int(earg)
+ except ValueError:
+ pass
+ else:
+ ename, earg = e, None
+ extras[ename] = earg
+ else:
+ extras = None
+
+ if opts.link_patterns_file:
+ link_patterns = []
+ f = open(opts.link_patterns_file)
+ try:
+ for i, line in enumerate(f.readlines()):
+ if not line.strip(): continue
+ if line.lstrip().startswith("#"): continue
+ try:
+ pat, href = line.rstrip().rsplit(None, 1)
+ except ValueError:
+ raise MarkdownError("%s:%d: invalid link pattern line: %r"
+ % (opts.link_patterns_file, i+1, line))
+ link_patterns.append(
+ (_regex_from_encoded_pattern(pat), href))
+ finally:
+ f.close()
+ else:
+ link_patterns = None
+
+ from os.path import join, dirname, abspath, exists
+ markdown_pl = join(dirname(dirname(abspath(__file__))), "test",
+ "Markdown.pl")
+ for path in paths:
+ if opts.compare:
+ print "==== Markdown.pl ===="
+ perl_cmd = 'perl %s "%s"' % (markdown_pl, path)
+ o = os.popen(perl_cmd)
+ perl_html = o.read()
+ o.close()
+ sys.stdout.write(perl_html)
+ print "==== markdown2.py ===="
+ html = markdown_path(path, encoding=opts.encoding,
+ html4tags=opts.html4tags,
+ safe_mode=opts.safe_mode,
+ extras=extras, link_patterns=link_patterns,
+ use_file_vars=opts.use_file_vars)
+ sys.stdout.write(
+ html.encode(sys.stdout.encoding or "utf-8", 'xmlcharrefreplace'))
+ if opts.compare:
+ test_dir = join(dirname(dirname(abspath(__file__))), "test")
+ if exists(join(test_dir, "test_markdown2.py")):
+ sys.path.insert(0, test_dir)
+ from test_markdown2 import norm_html_from_html
+ norm_html = norm_html_from_html(html)
+ norm_perl_html = norm_html_from_html(perl_html)
+ else:
+ norm_html = html
+ norm_perl_html = perl_html
+ print "==== match? %r ====" % (norm_perl_html == norm_html)
+
+
+if __name__ == "__main__":
+ sys.exit( main(sys.argv) )
+
diff --git a/vendor/tornado/demos/appengine/static/blog.css b/vendor/tornado/demos/appengine/static/blog.css
new file mode 100644
index 0000000000..8902ec1f22
--- /dev/null
+++ b/vendor/tornado/demos/appengine/static/blog.css
@@ -0,0 +1,153 @@
+/*
+ * Copyright 2009 Facebook
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License. You may obtain
+ * a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+body {
+ background: white;
+ color: black;
+ margin: 15px;
+ margin-top: 0;
+}
+
+body,
+input,
+textarea {
+ font-family: Georgia, serif;
+ font-size: 12pt;
+}
+
+table {
+ border-collapse: collapse;
+ border: 0;
+}
+
+td {
+ border: 0;
+ padding: 0;
+}
+
+h1,
+h2,
+h3,
+h4 {
+ font-family: "Helvetica Nue", Helvetica, Arial, sans-serif;
+ margin: 0;
+}
+
+h1 {
+ font-size: 20pt;
+}
+
+pre,
+code {
+ font-family: monospace;
+ color: #060;
+}
+
+pre {
+ margin-left: 1em;
+ padding-left: 1em;
+ border-left: 1px solid silver;
+ line-height: 14pt;
+}
+
+a,
+a code {
+ color: #00c;
+}
+
+#body {
+ max-width: 800px;
+ margin: auto;
+}
+
+#header {
+ background-color: #3b5998;
+ padding: 5px;
+ padding-left: 10px;
+ padding-right: 10px;
+ margin-bottom: 1em;
+}
+
+#header,
+#header a {
+ color: white;
+}
+
+#header h1 a {
+ text-decoration: none;
+}
+
+#footer,
+#content {
+ margin-left: 10px;
+ margin-right: 10px;
+}
+
+#footer {
+ margin-top: 3em;
+}
+
+.entry h1 a {
+ color: black;
+ text-decoration: none;
+}
+
+.entry {
+ margin-bottom: 2em;
+}
+
+.entry .date {
+ margin-top: 3px;
+}
+
+.entry p {
+ margin: 0;
+ margin-bottom: 1em;
+}
+
+.entry .body {
+ margin-top: 1em;
+ line-height: 16pt;
+}
+
+.compose td {
+ vertical-align: middle;
+ padding-bottom: 5px;
+}
+
+.compose td.field {
+ padding-right: 10px;
+}
+
+.compose .title,
+.compose .submit {
+ font-family: "Helvetica Nue", Helvetica, Arial, sans-serif;
+ font-weight: bold;
+}
+
+.compose .title {
+ font-size: 20pt;
+}
+
+.compose .title,
+.compose .markdown {
+ width: 100%;
+}
+
+.compose .markdown {
+ height: 500px;
+ line-height: 16pt;
+}
diff --git a/vendor/tornado/demos/appengine/templates/archive.html b/vendor/tornado/demos/appengine/templates/archive.html
new file mode 100644
index 0000000000..dcca9511a4
--- /dev/null
+++ b/vendor/tornado/demos/appengine/templates/archive.html
@@ -0,0 +1,31 @@
+{% extends "base.html" %}
+
+{% block head %}
+ <style type="text/css">
+ ul.archive {
+ list-style-type: none;
+ margin: 0;
+ padding: 0;
+ }
+
+ ul.archive li {
+ margin-bottom: 1em;
+ }
+
+ ul.archive .title {
+ font-family: "Helvetica Nue", Helvetica, Arial, sans-serif;
+ font-size: 14pt;
+ }
+ </style>
+{% end %}
+
+{% block body %}
+ <ul class="archive">
+ {% for entry in entries %}
+ <li>
+ <div class="title"><a href="/entry/{{ entry.slug }}">{{ escape(entry.title) }}</a></div>
+ <div class="date">{{ locale.format_date(entry.published, full_format=True, shorter=True) }}</div>
+ </li>
+ {% end %}
+ </ul>
+{% end %}
diff --git a/vendor/tornado/demos/appengine/templates/base.html b/vendor/tornado/demos/appengine/templates/base.html
new file mode 100644
index 0000000000..0154aea8ca
--- /dev/null
+++ b/vendor/tornado/demos/appengine/templates/base.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>{{ escape(handler.settings["blog_title"]) }}</title>
+ <link rel="stylesheet" href="/static/blog.css" type="text/css"/>
+ <link rel="alternate" href="/feed" type="application/atom+xml" title="{{ escape(handler.settings["blog_title"]) }}"/>
+ {% block head %}{% end %}
+ </head>
+ <body>
+ <div id="body">
+ <div id="header">
+ <div style="float:right">
+ {% if not current_user %}
+ {{ _('<a href="%(url)s">Sign in</a> to compose/edit') % {"url": escape(users.create_login_url(request.uri))} }}
+ {% else %}
+ {% if current_user.administrator %}
+ <a href="/compose">{{ _("New post") }}</a> -
+ {% end %}
+ <a href="{{ escape(users.create_logout_url(request.uri)) }}">{{ _("Sign out") }}</a>
+ {% end %}
+ </div>
+ <h1><a href="/">{{ escape(handler.settings["blog_title"]) }}</a></h1>
+ </div>
+ <div id="content">{% block body %}{% end %}</div>
+ </div>
+ {% block bottom %}{% end %}
+ </body>
+</html>
diff --git a/vendor/tornado/demos/appengine/templates/compose.html b/vendor/tornado/demos/appengine/templates/compose.html
new file mode 100644
index 0000000000..5ad548307c
--- /dev/null
+++ b/vendor/tornado/demos/appengine/templates/compose.html
@@ -0,0 +1,42 @@
+{% extends "base.html" %}
+
+{% block body %}
+ <form action="{{ request.path }}" method="post" class="compose">
+ <div style="margin-bottom:5px"><input name="title" type="text" class="title" value="{{ escape(entry.title) if entry else "" }}"/></div>
+ <div style="margin-bottom:5px"><textarea name="markdown" rows="30" cols="40" class="markdown">{{ escape(entry.markdown) if entry else "" }}</textarea></div>
+ <div>
+ <div style="float:right"><a href="http://daringfireball.net/projects/markdown/syntax">{{ _("Syntax documentation") }}</a></div>
+ <input type="submit" value="{{ _("Save changes") if entry else _("Publish post") }}" class="submit"/>
+ &nbsp;<a href="{{ "/entry/" + entry.slug if entry else "/" }}">{{ _("Cancel") }}</a>
+ </div>
+ {% if entry %}
+ <input type="hidden" name="key" value="{{ str(entry.key()) }}"/>
+ {% end %}
+ {{ xsrf_form_html() }}
+ </form>
+{% end %}
+
+{% block bottom %}
+ <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js" type="text/javascript"></script>
+ <script type="text/javascript">
+ //<![CDATA[
+
+ $(function() {
+ $("input[name=title]").select();
+ $("form.compose").submit(function() {
+ var required = ["title", "markdown"];
+ var form = $(this).get(0);
+ for (var i = 0; i < required.length; i++) {
+ if (!form[required[i]].value) {
+ $(form[required[i]]).select();
+ return false;
+ }
+ }
+ return true;
+ });
+ });
+
+ //]]>
+ </script>
+{% end %}
+
diff --git a/vendor/tornado/demos/appengine/templates/entry.html b/vendor/tornado/demos/appengine/templates/entry.html
new file mode 100644
index 0000000000..43c835dead
--- /dev/null
+++ b/vendor/tornado/demos/appengine/templates/entry.html
@@ -0,0 +1,5 @@
+{% extends "base.html" %}
+
+{% block body %}
+ {{ modules.Entry(entry) }}
+{% end %}
diff --git a/vendor/tornado/demos/appengine/templates/feed.xml b/vendor/tornado/demos/appengine/templates/feed.xml
new file mode 100644
index 0000000000..c6c368656c
--- /dev/null
+++ b/vendor/tornado/demos/appengine/templates/feed.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<feed xmlns="http://www.w3.org/2005/Atom">
+ {% set date_format = "%Y-%m-%dT%H:%M:%SZ" %}
+ <title>{{ escape(handler.settings["blog_title"]) }}</title>
+ {% if len(entries) > 0 %}
+ <updated>{{ max(e.updated for e in entries).strftime(date_format) }}</updated>
+ {% else %}
+ <updated>{{ datetime.datetime.utcnow().strftime(date_format) }}</updated>
+ {% end %}
+ <id>http://{{ request.host }}/</id>
+ <link rel="alternate" href="http://{{ request.host }}/" title="{{ escape(handler.settings["blog_title"]) }}" type="text/html"/>
+ <link rel="self" href="{{ request.full_url() }}" title="{{ escape(handler.settings["blog_title"]) }}" type="application/atom+xml"/>
+ <author><name>{{ escape(handler.settings["blog_title"]) }}</name></author>
+ {% for entry in entries %}
+ <entry>
+ <id>http://{{ request.host }}/entry/{{ entry.slug }}</id>
+ <title type="text">{{ escape(entry.title) }}</title>
+ <link href="http://{{ request.host }}/entry/{{ entry.slug }}" rel="alternate" type="text/html"/>
+ <updated>{{ entry.updated.strftime(date_format) }}</updated>
+ <published>{{ entry.published.strftime(date_format) }}</published>
+ <content type="xhtml" xml:base="http://{{ request.host }}/">
+ <div xmlns="http://www.w3.org/1999/xhtml">{{ entry.html }}</div>
+ </content>
+ </entry>
+ {% end %}
+</feed>
diff --git a/vendor/tornado/demos/appengine/templates/home.html b/vendor/tornado/demos/appengine/templates/home.html
new file mode 100644
index 0000000000..dd069a97f3
--- /dev/null
+++ b/vendor/tornado/demos/appengine/templates/home.html
@@ -0,0 +1,8 @@
+{% extends "base.html" %}
+
+{% block body %}
+ {% for entry in entries %}
+ {{ modules.Entry(entry) }}
+ {% end %}
+ <div><a href="/archive">{{ _("Archive") }}</a></div>
+{% end %}
diff --git a/vendor/tornado/demos/appengine/templates/modules/entry.html b/vendor/tornado/demos/appengine/templates/modules/entry.html
new file mode 100644
index 0000000000..06237657c8
--- /dev/null
+++ b/vendor/tornado/demos/appengine/templates/modules/entry.html
@@ -0,0 +1,8 @@
+<div class="entry">
+ <h1><a href="/entry/{{ entry.slug }}">{{ escape(entry.title) }}</a></h1>
+ <div class="date">{{ locale.format_date(entry.published, full_format=True, shorter=True) }}</div>
+ <div class="body">{{ entry.html }}</div>
+ {% if current_user and current_user.administrator %}
+ <div class="admin"><a href="/compose?key={{ str(entry.key()) }}">{{ _("Edit this post") }}</a></div>
+ {% end %}
+</div>
diff --git a/vendor/tornado/demos/auth/authdemo.py b/vendor/tornado/demos/auth/authdemo.py
new file mode 100755
index 0000000000..e6136d1b53
--- /dev/null
+++ b/vendor/tornado/demos/auth/authdemo.py
@@ -0,0 +1,79 @@
+#!/usr/bin/env python
+#
+# Copyright 2009 Facebook
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import tornado.auth
+import tornado.escape
+import tornado.httpserver
+import tornado.ioloop
+import tornado.options
+import tornado.web
+
+from tornado.options import define, options
+
+define("port", default=8888, help="run on the given port", type=int)
+
+
+class Application(tornado.web.Application):
+ def __init__(self):
+ handlers = [
+ (r"/", MainHandler),
+ (r"/auth/login", AuthHandler),
+ ]
+ settings = dict(
+ cookie_secret="32oETzKXQAGaYdkL5gEmGeJJFuYh7EQnp2XdTP1o/Vo=",
+ login_url="/auth/login",
+ )
+ tornado.web.Application.__init__(self, handlers, **settings)
+
+
+class BaseHandler(tornado.web.RequestHandler):
+ def get_current_user(self):
+ user_json = self.get_secure_cookie("user")
+ if not user_json: return None
+ return tornado.escape.json_decode(user_json)
+
+
+class MainHandler(BaseHandler):
+ @tornado.web.authenticated
+ def get(self):
+ name = tornado.escape.xhtml_escape(self.current_user["name"])
+ self.write("Hello, " + name)
+
+
+class AuthHandler(BaseHandler, tornado.auth.GoogleMixin):
+ @tornado.web.asynchronous
+ def get(self):
+ if self.get_argument("openid.mode", None):
+ self.get_authenticated_user(self.async_callback(self._on_auth))
+ return
+ self.authenticate_redirect()
+
+ def _on_auth(self, user):
+ if not user:
+ raise tornado.web.HTTPError(500, "Google auth failed")
+ self.set_secure_cookie("user", tornado.escape.json_encode(user))
+ self.redirect("/")
+
+
+def main():
+ tornado.options.parse_command_line()
+ http_server = tornado.httpserver.HTTPServer(Application())
+ http_server.listen(options.port)
+ tornado.ioloop.IOLoop.instance().start()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/vendor/tornado/demos/blog/README b/vendor/tornado/demos/blog/README
new file mode 100644
index 0000000000..a033e7a11c
--- /dev/null
+++ b/vendor/tornado/demos/blog/README
@@ -0,0 +1,57 @@
+Running the Tornado Blog example app
+====================================
+This demo is a simple blogging engine that uses MySQL to store posts and
+Google Accounts for author authentication. Since it depends on MySQL, you
+need to set up MySQL and the database schema for the demo to run.
+
+1. Install prerequisites and build tornado
+
+ See http://www.tornadoweb.org/ for installation instructions. If you can
+ run the "helloworld" example application, your environment is set up
+ correctly.
+
+2. Install MySQL if needed
+
+ Consult the documentation for your platform. Under Ubuntu Linux you
+ can run "apt-get install mysql". Under OS X you can download the
+ MySQL PKG file from http://dev.mysql.com/downloads/mysql/
+
+3. Connect to MySQL and create a database and user for the blog.
+
+ Connect to MySQL as a user that can create databases and users:
+ mysql -u root
+
+ Create a database named "blog":
+ mysql> CREATE DATABASE blog;
+
+ Allow the "blog" user to connect with the password "blog":
+ mysql> GRANT ALL PRIVILEGES ON blog.* TO 'blog'@'localhost' IDENTIFIED BY 'blog';
+
+4. Create the tables in your new database.
+
+ You can use the provided schema.sql file by running this command:
+ mysql --user=blog --password=blog --database=blog < schema.sql
+
+ You can run the above command again later if you want to delete the
+ contents of the blog and start over after testing.
+
+5. Run the blog example
+
+ With the default user, password, and database you can just run:
+ ./blog.py
+
+ If you've changed anything, you can alter the default MySQL settings
+ with arguments on the command line, e.g.:
+ ./blog.py --mysql_user=casey --mysql_password=happiness --mysql_database=foodblog
+
+6. Visit your new blog
+
+ Open http://localhost:8888/ in your web browser. You will be redirected to
+ a Google account sign-in page because the blog uses Google accounts for
+ authentication.
+
+ Currently the first user to connect will automatically be given the
+ ability to create and edit posts.
+
+ Once you've created one blog post, subsequent users will not be
+ prompted to sign in.
diff --git a/vendor/tornado/demos/blog/blog.py b/vendor/tornado/demos/blog/blog.py
new file mode 100755
index 0000000000..808a9afc55
--- /dev/null
+++ b/vendor/tornado/demos/blog/blog.py
@@ -0,0 +1,195 @@
+#!/usr/bin/env python
+#
+# Copyright 2009 Facebook
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import markdown
+import os.path
+import re
+import tornado.auth
+import tornado.database
+import tornado.httpserver
+import tornado.ioloop
+import tornado.options
+import tornado.web
+import unicodedata
+
+from tornado.options import define, options
+
+define("port", default=8888, help="run on the given port", type=int)
+define("mysql_host", default="127.0.0.1:3306", help="blog database host")
+define("mysql_database", default="blog", help="blog database name")
+define("mysql_user", default="blog", help="blog database user")
+define("mysql_password", default="blog", help="blog database password")
+
+
+class Application(tornado.web.Application):
+ def __init__(self):
+ handlers = [
+ (r"/", HomeHandler),
+ (r"/archive", ArchiveHandler),
+ (r"/feed", FeedHandler),
+ (r"/entry/([^/]+)", EntryHandler),
+ (r"/compose", ComposeHandler),
+ (r"/auth/login", AuthLoginHandler),
+ (r"/auth/logout", AuthLogoutHandler),
+ ]
+ settings = dict(
+ blog_title=u"Tornado Blog",
+ template_path=os.path.join(os.path.dirname(__file__), "templates"),
+ static_path=os.path.join(os.path.dirname(__file__), "static"),
+ ui_modules={"Entry": EntryModule},
+ xsrf_cookies=True,
+ cookie_secret="11oETzKXQAGaYdkL5gEmGeJJFuYh7EQnp2XdTP1o/Vo=",
+ login_url="/auth/login",
+ )
+ tornado.web.Application.__init__(self, handlers, **settings)
+
+ # Have one global connection to the blog DB across all handlers
+ self.db = tornado.database.Connection(
+ host=options.mysql_host, database=options.mysql_database,
+ user=options.mysql_user, password=options.mysql_password)
+
+
+class BaseHandler(tornado.web.RequestHandler):
+ @property
+ def db(self):
+ return self.application.db
+
+ def get_current_user(self):
+ user_id = self.get_secure_cookie("user")
+ if not user_id: return None
+ return self.db.get("SELECT * FROM authors WHERE id = %s", int(user_id))
+
+
+class HomeHandler(BaseHandler):
+ def get(self):
+ entries = self.db.query("SELECT * FROM entries ORDER BY published "
+ "DESC LIMIT 5")
+ if not entries:
+ self.redirect("/compose")
+ return
+ self.render("home.html", entries=entries)
+
+
+class EntryHandler(BaseHandler):
+ def get(self, slug):
+ entry = self.db.get("SELECT * FROM entries WHERE slug = %s", slug)
+ if not entry: raise tornado.web.HTTPError(404)
+ self.render("entry.html", entry=entry)
+
+
+class ArchiveHandler(BaseHandler):
+ def get(self):
+ entries = self.db.query("SELECT * FROM entries ORDER BY published "
+ "DESC")
+ self.render("archive.html", entries=entries)
+
+
+class FeedHandler(BaseHandler):
+ def get(self):
+ entries = self.db.query("SELECT * FROM entries ORDER BY published "
+ "DESC LIMIT 10")
+ self.set_header("Content-Type", "application/atom+xml")
+ self.render("feed.xml", entries=entries)
+
+
+class ComposeHandler(BaseHandler):
+ @tornado.web.authenticated
+ def get(self):
+ id = self.get_argument("id", None)
+ entry = None
+ if id:
+ entry = self.db.get("SELECT * FROM entries WHERE id = %s", int(id))
+ self.render("compose.html", entry=entry)
+
+ @tornado.web.authenticated
+ def post(self):
+ id = self.get_argument("id", None)
+ title = self.get_argument("title")
+ text = self.get_argument("markdown")
+ html = markdown.markdown(text)
+ if id:
+ entry = self.db.get("SELECT * FROM entries WHERE id = %s", int(id))
+ if not entry: raise tornado.web.HTTPError(404)
+ slug = entry.slug
+ self.db.execute(
+ "UPDATE entries SET title = %s, markdown = %s, html = %s "
+ "WHERE id = %s", title, text, html, int(id))
+ else:
+ slug = unicodedata.normalize("NFKD", title).encode(
+ "ascii", "ignore")
+ slug = re.sub(r"[^\w]+", " ", slug)
+ slug = "-".join(slug.lower().strip().split())
+ if not slug: slug = "entry"
+ while True:
+ e = self.db.get("SELECT * FROM entries WHERE slug = %s", slug)
+ if not e: break
+ slug += "-2"
+ self.db.execute(
+ "INSERT INTO entries (author_id,title,slug,markdown,html,"
+ "published) VALUES (%s,%s,%s,%s,%s,UTC_TIMESTAMP())",
+ self.current_user.id, title, slug, text, html)
+ self.redirect("/entry/" + slug)
+
+
+class AuthLoginHandler(BaseHandler, tornado.auth.GoogleMixin):
+ @tornado.web.asynchronous
+ def get(self):
+ if self.get_argument("openid.mode", None):
+ self.get_authenticated_user(self.async_callback(self._on_auth))
+ return
+ self.authenticate_redirect()
+
+ def _on_auth(self, user):
+ if not user:
+ raise tornado.web.HTTPError(500, "Google auth failed")
+ author = self.db.get("SELECT * FROM authors WHERE email = %s",
+ user["email"])
+ if not author:
+ # Auto-create first author
+ any_author = self.db.get("SELECT * FROM authors LIMIT 1")
+ if not any_author:
+ author_id = self.db.execute(
+ "INSERT INTO authors (email,name) VALUES (%s,%s)",
+ user["email"], user["name"])
+ else:
+ self.redirect("/")
+ return
+ else:
+ author_id = author["id"]
+ self.set_secure_cookie("user", str(author_id))
+ self.redirect(self.get_argument("next", "/"))
+
+
+class AuthLogoutHandler(BaseHandler):
+ def get(self):
+ self.clear_cookie("user")
+ self.redirect(self.get_argument("next", "/"))
+
+
+class EntryModule(tornado.web.UIModule):
+ def render(self, entry):
+ return self.render_string("modules/entry.html", entry=entry)
+
+
+def main():
+ tornado.options.parse_command_line()
+ http_server = tornado.httpserver.HTTPServer(Application())
+ http_server.listen(options.port)
+ tornado.ioloop.IOLoop.instance().start()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/vendor/tornado/demos/blog/markdown.py b/vendor/tornado/demos/blog/markdown.py
new file mode 100644
index 0000000000..59ba731bf0
--- /dev/null
+++ b/vendor/tornado/demos/blog/markdown.py
@@ -0,0 +1,1877 @@
+#!/usr/bin/env python
+# Copyright (c) 2007-2008 ActiveState Corp.
+# License: MIT (http://www.opensource.org/licenses/mit-license.php)
+
+r"""A fast and complete Python implementation of Markdown.
+
+[from http://daringfireball.net/projects/markdown/]
+> Markdown is a text-to-HTML filter; it translates an easy-to-read /
+> easy-to-write structured text format into HTML. Markdown's text
+> format is most similar to that of plain text email, and supports
+> features such as headers, *emphasis*, code blocks, blockquotes, and
+> links.
+>
+> Markdown's syntax is designed not as a generic markup language, but
+> specifically to serve as a front-end to (X)HTML. You can use span-level
+> HTML tags anywhere in a Markdown document, and you can use block level
+> HTML tags (like <div> and <table> as well).
+
+Module usage:
+
+ >>> import markdown2
+ >>> markdown2.markdown("*boo!*") # or use `html = markdown_path(PATH)`
+ u'<p><em>boo!</em></p>\n'
+
+ >>> markdowner = Markdown()
+ >>> markdowner.convert("*boo!*")
+ u'<p><em>boo!</em></p>\n'
+ >>> markdowner.convert("**boom!**")
+ u'<p><strong>boom!</strong></p>\n'
+
+This implementation of Markdown implements the full "core" syntax plus a
+number of extras (e.g., code syntax coloring, footnotes) as described on
+<http://code.google.com/p/python-markdown2/wiki/Extras>.
+"""
+
+cmdln_desc = """A fast and complete Python implementation of Markdown, a
+text-to-HTML conversion tool for web writers.
+"""
+
+# Dev Notes:
+# - There is already a Python markdown processor
+# (http://www.freewisdom.org/projects/python-markdown/).
+# - Python's regex syntax doesn't have '\z', so I'm using '\Z'. I'm
+# not yet sure if there implications with this. Compare 'pydoc sre'
+# and 'perldoc perlre'.
+
+__version_info__ = (1, 0, 1, 14) # first three nums match Markdown.pl
+__version__ = '1.0.1.14'
+__author__ = "Trent Mick"
+
+import os
+import sys
+from pprint import pprint
+import re
+import logging
+try:
+ from hashlib import md5
+except ImportError:
+ from md5 import md5
+import optparse
+from random import random
+import codecs
+
+
+
+#---- Python version compat
+
+if sys.version_info[:2] < (2,4):
+ from sets import Set as set
+ def reversed(sequence):
+ for i in sequence[::-1]:
+ yield i
+ def _unicode_decode(s, encoding, errors='xmlcharrefreplace'):
+ return unicode(s, encoding, errors)
+else:
+ def _unicode_decode(s, encoding, errors='strict'):
+ return s.decode(encoding, errors)
+
+
+#---- globals
+
+DEBUG = False
+log = logging.getLogger("markdown")
+
+DEFAULT_TAB_WIDTH = 4
+
+# Table of hash values for escaped characters:
+def _escape_hash(s):
+ # Lame attempt to avoid possible collision with someone actually
+ # using the MD5 hexdigest of one of these chars in there text.
+ # Other ideas: random.random(), uuid.uuid()
+ #return md5(s).hexdigest() # Markdown.pl effectively does this.
+ return 'md5-'+md5(s).hexdigest()
+g_escape_table = dict([(ch, _escape_hash(ch))
+ for ch in '\\`*_{}[]()>#+-.!'])
+
+
+
+#---- exceptions
+
+class MarkdownError(Exception):
+ pass
+
+
+
+#---- public api
+
+def markdown_path(path, encoding="utf-8",
+ html4tags=False, tab_width=DEFAULT_TAB_WIDTH,
+ safe_mode=None, extras=None, link_patterns=None,
+ use_file_vars=False):
+ text = codecs.open(path, 'r', encoding).read()
+ return Markdown(html4tags=html4tags, tab_width=tab_width,
+ safe_mode=safe_mode, extras=extras,
+ link_patterns=link_patterns,
+ use_file_vars=use_file_vars).convert(text)
+
+def markdown(text, html4tags=False, tab_width=DEFAULT_TAB_WIDTH,
+ safe_mode=None, extras=None, link_patterns=None,
+ use_file_vars=False):
+ return Markdown(html4tags=html4tags, tab_width=tab_width,
+ safe_mode=safe_mode, extras=extras,
+ link_patterns=link_patterns,
+ use_file_vars=use_file_vars).convert(text)
+
+class Markdown(object):
+ # The dict of "extras" to enable in processing -- a mapping of
+ # extra name to argument for the extra. Most extras do not have an
+ # argument, in which case the value is None.
+ #
+ # This can be set via (a) subclassing and (b) the constructor
+ # "extras" argument.
+ extras = None
+
+ urls = None
+ titles = None
+ html_blocks = None
+ html_spans = None
+ html_removed_text = "[HTML_REMOVED]" # for compat with markdown.py
+
+ # Used to track when we're inside an ordered or unordered list
+ # (see _ProcessListItems() for details):
+ list_level = 0
+
+ _ws_only_line_re = re.compile(r"^[ \t]+$", re.M)
+
+ def __init__(self, html4tags=False, tab_width=4, safe_mode=None,
+ extras=None, link_patterns=None, use_file_vars=False):
+ if html4tags:
+ self.empty_element_suffix = ">"
+ else:
+ self.empty_element_suffix = " />"
+ self.tab_width = tab_width
+
+ # For compatibility with earlier markdown2.py and with
+ # markdown.py's safe_mode being a boolean,
+ # safe_mode == True -> "replace"
+ if safe_mode is True:
+ self.safe_mode = "replace"
+ else:
+ self.safe_mode = safe_mode
+
+ if self.extras is None:
+ self.extras = {}
+ elif not isinstance(self.extras, dict):
+ self.extras = dict([(e, None) for e in self.extras])
+ if extras:
+ if not isinstance(extras, dict):
+ extras = dict([(e, None) for e in extras])
+ self.extras.update(extras)
+ assert isinstance(self.extras, dict)
+ self._instance_extras = self.extras.copy()
+ self.link_patterns = link_patterns
+ self.use_file_vars = use_file_vars
+ self._outdent_re = re.compile(r'^(\t|[ ]{1,%d})' % tab_width, re.M)
+
+ def reset(self):
+ self.urls = {}
+ self.titles = {}
+ self.html_blocks = {}
+ self.html_spans = {}
+ self.list_level = 0
+ self.extras = self._instance_extras.copy()
+ if "footnotes" in self.extras:
+ self.footnotes = {}
+ self.footnote_ids = []
+
+ def convert(self, text):
+ """Convert the given text."""
+ # Main function. The order in which other subs are called here is
+ # essential. Link and image substitutions need to happen before
+ # _EscapeSpecialChars(), so that any *'s or _'s in the <a>
+ # and <img> tags get encoded.
+
+ # Clear the global hashes. If we don't clear these, you get conflicts
+ # from other articles when generating a page which contains more than
+ # one article (e.g. an index page that shows the N most recent
+ # articles):
+ self.reset()
+
+ if not isinstance(text, unicode):
+ #TODO: perhaps shouldn't presume UTF-8 for string input?
+ text = unicode(text, 'utf-8')
+
+ if self.use_file_vars:
+ # Look for emacs-style file variable hints.
+ emacs_vars = self._get_emacs_vars(text)
+ if "markdown-extras" in emacs_vars:
+ splitter = re.compile("[ ,]+")
+ for e in splitter.split(emacs_vars["markdown-extras"]):
+ if '=' in e:
+ ename, earg = e.split('=', 1)
+ try:
+ earg = int(earg)
+ except ValueError:
+ pass
+ else:
+ ename, earg = e, None
+ self.extras[ename] = earg
+
+ # Standardize line endings:
+ text = re.sub("\r\n|\r", "\n", text)
+
+ # Make sure $text ends with a couple of newlines:
+ text += "\n\n"
+
+ # Convert all tabs to spaces.
+ text = self._detab(text)
+
+ # Strip any lines consisting only of spaces and tabs.
+ # This makes subsequent regexen easier to write, because we can
+ # match consecutive blank lines with /\n+/ instead of something
+ # contorted like /[ \t]*\n+/ .
+ text = self._ws_only_line_re.sub("", text)
+
+ if self.safe_mode:
+ text = self._hash_html_spans(text)
+
+ # Turn block-level HTML blocks into hash entries
+ text = self._hash_html_blocks(text, raw=True)
+
+ # Strip link definitions, store in hashes.
+ if "footnotes" in self.extras:
+ # Must do footnotes first because an unlucky footnote defn
+ # looks like a link defn:
+ # [^4]: this "looks like a link defn"
+ text = self._strip_footnote_definitions(text)
+ text = self._strip_link_definitions(text)
+
+ text = self._run_block_gamut(text)
+
+ if "footnotes" in self.extras:
+ text = self._add_footnotes(text)
+
+ text = self._unescape_special_chars(text)
+
+ if self.safe_mode:
+ text = self._unhash_html_spans(text)
+
+ text += "\n"
+ return text
+
+ _emacs_oneliner_vars_pat = re.compile(r"-\*-\s*([^\r\n]*?)\s*-\*-", re.UNICODE)
+ # This regular expression is intended to match blocks like this:
+ # PREFIX Local Variables: SUFFIX
+ # PREFIX mode: Tcl SUFFIX
+ # PREFIX End: SUFFIX
+ # Some notes:
+ # - "[ \t]" is used instead of "\s" to specifically exclude newlines
+ # - "(\r\n|\n|\r)" is used instead of "$" because the sre engine does
+ # not like anything other than Unix-style line terminators.
+ _emacs_local_vars_pat = re.compile(r"""^
+ (?P<prefix>(?:[^\r\n|\n|\r])*?)
+ [\ \t]*Local\ Variables:[\ \t]*
+ (?P<suffix>.*?)(?:\r\n|\n|\r)
+ (?P<content>.*?\1End:)
+ """, re.IGNORECASE | re.MULTILINE | re.DOTALL | re.VERBOSE)
+
+ def _get_emacs_vars(self, text):
+ """Return a dictionary of emacs-style local variables.
+
+ Parsing is done loosely according to this spec (and according to
+ some in-practice deviations from this):
+ http://www.gnu.org/software/emacs/manual/html_node/emacs/Specifying-File-Variables.html#Specifying-File-Variables
+ """
+ emacs_vars = {}
+ SIZE = pow(2, 13) # 8kB
+
+ # Search near the start for a '-*-'-style one-liner of variables.
+ head = text[:SIZE]
+ if "-*-" in head:
+ match = self._emacs_oneliner_vars_pat.search(head)
+ if match:
+ emacs_vars_str = match.group(1)
+ assert '\n' not in emacs_vars_str
+ emacs_var_strs = [s.strip() for s in emacs_vars_str.split(';')
+ if s.strip()]
+ if len(emacs_var_strs) == 1 and ':' not in emacs_var_strs[0]:
+ # While not in the spec, this form is allowed by emacs:
+ # -*- Tcl -*-
+ # where the implied "variable" is "mode". This form
+ # is only allowed if there are no other variables.
+ emacs_vars["mode"] = emacs_var_strs[0].strip()
+ else:
+ for emacs_var_str in emacs_var_strs:
+ try:
+ variable, value = emacs_var_str.strip().split(':', 1)
+ except ValueError:
+ log.debug("emacs variables error: malformed -*- "
+ "line: %r", emacs_var_str)
+ continue
+ # Lowercase the variable name because Emacs allows "Mode"
+ # or "mode" or "MoDe", etc.
+ emacs_vars[variable.lower()] = value.strip()
+
+ tail = text[-SIZE:]
+ if "Local Variables" in tail:
+ match = self._emacs_local_vars_pat.search(tail)
+ if match:
+ prefix = match.group("prefix")
+ suffix = match.group("suffix")
+ lines = match.group("content").splitlines(0)
+ #print "prefix=%r, suffix=%r, content=%r, lines: %s"\
+ # % (prefix, suffix, match.group("content"), lines)
+
+ # Validate the Local Variables block: proper prefix and suffix
+ # usage.
+ for i, line in enumerate(lines):
+ if not line.startswith(prefix):
+ log.debug("emacs variables error: line '%s' "
+ "does not use proper prefix '%s'"
+ % (line, prefix))
+ return {}
+ # Don't validate suffix on last line. Emacs doesn't care,
+ # neither should we.
+ if i != len(lines)-1 and not line.endswith(suffix):
+ log.debug("emacs variables error: line '%s' "
+ "does not use proper suffix '%s'"
+ % (line, suffix))
+ return {}
+
+ # Parse out one emacs var per line.
+ continued_for = None
+ for line in lines[:-1]: # no var on the last line ("PREFIX End:")
+ if prefix: line = line[len(prefix):] # strip prefix
+ if suffix: line = line[:-len(suffix)] # strip suffix
+ line = line.strip()
+ if continued_for:
+ variable = continued_for
+ if line.endswith('\\'):
+ line = line[:-1].rstrip()
+ else:
+ continued_for = None
+ emacs_vars[variable] += ' ' + line
+ else:
+ try:
+ variable, value = line.split(':', 1)
+ except ValueError:
+ log.debug("local variables error: missing colon "
+ "in local variables entry: '%s'" % line)
+ continue
+ # Do NOT lowercase the variable name, because Emacs only
+ # allows "mode" (and not "Mode", "MoDe", etc.) in this block.
+ value = value.strip()
+ if value.endswith('\\'):
+ value = value[:-1].rstrip()
+ continued_for = variable
+ else:
+ continued_for = None
+ emacs_vars[variable] = value
+
+ # Unquote values.
+ for var, val in emacs_vars.items():
+ if len(val) > 1 and (val.startswith('"') and val.endswith('"')
+ or val.startswith('"') and val.endswith('"')):
+ emacs_vars[var] = val[1:-1]
+
+ return emacs_vars
+
+ # Cribbed from a post by Bart Lateur:
+ # <http://www.nntp.perl.org/group/perl.macperl.anyperl/154>
+ _detab_re = re.compile(r'(.*?)\t', re.M)
+ def _detab_sub(self, match):
+ g1 = match.group(1)
+ return g1 + (' ' * (self.tab_width - len(g1) % self.tab_width))
+ def _detab(self, text):
+ r"""Remove (leading?) tabs from a file.
+
+ >>> m = Markdown()
+ >>> m._detab("\tfoo")
+ ' foo'
+ >>> m._detab(" \tfoo")
+ ' foo'
+ >>> m._detab("\t foo")
+ ' foo'
+ >>> m._detab(" foo")
+ ' foo'
+ >>> m._detab(" foo\n\tbar\tblam")
+ ' foo\n bar blam'
+ """
+ if '\t' not in text:
+ return text
+ return self._detab_re.subn(self._detab_sub, text)[0]
+
+ _block_tags_a = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math|ins|del'
+ _strict_tag_block_re = re.compile(r"""
+ ( # save in \1
+ ^ # start of line (with re.M)
+ <(%s) # start tag = \2
+ \b # word break
+ (.*\n)*? # any number of lines, minimally matching
+ </\2> # the matching end tag
+ [ \t]* # trailing spaces/tabs
+ (?=\n+|\Z) # followed by a newline or end of document
+ )
+ """ % _block_tags_a,
+ re.X | re.M)
+
+ _block_tags_b = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math'
+ _liberal_tag_block_re = re.compile(r"""
+ ( # save in \1
+ ^ # start of line (with re.M)
+ <(%s) # start tag = \2
+ \b # word break
+ (.*\n)*? # any number of lines, minimally matching
+ .*</\2> # the matching end tag
+ [ \t]* # trailing spaces/tabs
+ (?=\n+|\Z) # followed by a newline or end of document
+ )
+ """ % _block_tags_b,
+ re.X | re.M)
+
+ def _hash_html_block_sub(self, match, raw=False):
+ html = match.group(1)
+ if raw and self.safe_mode:
+ html = self._sanitize_html(html)
+ key = _hash_text(html)
+ self.html_blocks[key] = html
+ return "\n\n" + key + "\n\n"
+
+ def _hash_html_blocks(self, text, raw=False):
+ """Hashify HTML blocks
+
+ We only want to do this for block-level HTML tags, such as headers,
+ lists, and tables. That's because we still want to wrap <p>s around
+ "paragraphs" that are wrapped in non-block-level tags, such as anchors,
+ phrase emphasis, and spans. The list of tags we're looking for is
+ hard-coded.
+
+ @param raw {boolean} indicates if these are raw HTML blocks in
+ the original source. It makes a difference in "safe" mode.
+ """
+ if '<' not in text:
+ return text
+
+ # Pass `raw` value into our calls to self._hash_html_block_sub.
+ hash_html_block_sub = _curry(self._hash_html_block_sub, raw=raw)
+
+ # First, look for nested blocks, e.g.:
+ # <div>
+ # <div>
+ # tags for inner block must be indented.
+ # </div>
+ # </div>
+ #
+ # The outermost tags must start at the left margin for this to match, and
+ # the inner nested divs must be indented.
+ # We need to do this before the next, more liberal match, because the next
+ # match will start at the first `<div>` and stop at the first `</div>`.
+ text = self._strict_tag_block_re.sub(hash_html_block_sub, text)
+
+ # Now match more liberally, simply from `\n<tag>` to `</tag>\n`
+ text = self._liberal_tag_block_re.sub(hash_html_block_sub, text)
+
+ # Special case just for <hr />. It was easier to make a special
+ # case than to make the other regex more complicated.
+ if "<hr" in text:
+ _hr_tag_re = _hr_tag_re_from_tab_width(self.tab_width)
+ text = _hr_tag_re.sub(hash_html_block_sub, text)
+
+ # Special case for standalone HTML comments:
+ if "<!--" in text:
+ start = 0
+ while True:
+ # Delimiters for next comment block.
+ try:
+ start_idx = text.index("<!--", start)
+ except ValueError, ex:
+ break
+ try:
+ end_idx = text.index("-->", start_idx) + 3
+ except ValueError, ex:
+ break
+
+ # Start position for next comment block search.
+ start = end_idx
+
+ # Validate whitespace before comment.
+ if start_idx:
+ # - Up to `tab_width - 1` spaces before start_idx.
+ for i in range(self.tab_width - 1):
+ if text[start_idx - 1] != ' ':
+ break
+ start_idx -= 1
+ if start_idx == 0:
+ break
+ # - Must be preceded by 2 newlines or hit the start of
+ # the document.
+ if start_idx == 0:
+ pass
+ elif start_idx == 1 and text[0] == '\n':
+ start_idx = 0 # to match minute detail of Markdown.pl regex
+ elif text[start_idx-2:start_idx] == '\n\n':
+ pass
+ else:
+ break
+
+ # Validate whitespace after comment.
+ # - Any number of spaces and tabs.
+ while end_idx < len(text):
+ if text[end_idx] not in ' \t':
+ break
+ end_idx += 1
+ # - Must be following by 2 newlines or hit end of text.
+ if text[end_idx:end_idx+2] not in ('', '\n', '\n\n'):
+ continue
+
+ # Escape and hash (must match `_hash_html_block_sub`).
+ html = text[start_idx:end_idx]
+ if raw and self.safe_mode:
+ html = self._sanitize_html(html)
+ key = _hash_text(html)
+ self.html_blocks[key] = html
+ text = text[:start_idx] + "\n\n" + key + "\n\n" + text[end_idx:]
+
+ if "xml" in self.extras:
+ # Treat XML processing instructions and namespaced one-liner
+ # tags as if they were block HTML tags. E.g., if standalone
+ # (i.e. are their own paragraph), the following do not get
+ # wrapped in a <p> tag:
+ # <?foo bar?>
+ #
+ # <xi:include xmlns:xi="http://www.w3.org/2001/XInclude" href="chapter_1.md"/>
+ _xml_oneliner_re = _xml_oneliner_re_from_tab_width(self.tab_width)
+ text = _xml_oneliner_re.sub(hash_html_block_sub, text)
+
+ return text
+
+ def _strip_link_definitions(self, text):
+ # Strips link definitions from text, stores the URLs and titles in
+ # hash references.
+ less_than_tab = self.tab_width - 1
+
+ # Link defs are in the form:
+ # [id]: url "optional title"
+ _link_def_re = re.compile(r"""
+ ^[ ]{0,%d}\[(.+)\]: # id = \1
+ [ \t]*
+ \n? # maybe *one* newline
+ [ \t]*
+ <?(.+?)>? # url = \2
+ [ \t]*
+ (?:
+ \n? # maybe one newline
+ [ \t]*
+ (?<=\s) # lookbehind for whitespace
+ ['"(]
+ ([^\n]*) # title = \3
+ ['")]
+ [ \t]*
+ )? # title is optional
+ (?:\n+|\Z)
+ """ % less_than_tab, re.X | re.M | re.U)
+ return _link_def_re.sub(self._extract_link_def_sub, text)
+
+ def _extract_link_def_sub(self, match):
+ id, url, title = match.groups()
+ key = id.lower() # Link IDs are case-insensitive
+ self.urls[key] = self._encode_amps_and_angles(url)
+ if title:
+ self.titles[key] = title.replace('"', '&quot;')
+ return ""
+
+ def _extract_footnote_def_sub(self, match):
+ id, text = match.groups()
+ text = _dedent(text, skip_first_line=not text.startswith('\n')).strip()
+ normed_id = re.sub(r'\W', '-', id)
+ # Ensure footnote text ends with a couple newlines (for some
+ # block gamut matches).
+ self.footnotes[normed_id] = text + "\n\n"
+ return ""
+
+ def _strip_footnote_definitions(self, text):
+ """A footnote definition looks like this:
+
+ [^note-id]: Text of the note.
+
+ May include one or more indented paragraphs.
+
+ Where,
+ - The 'note-id' can be pretty much anything, though typically it
+ is the number of the footnote.
+ - The first paragraph may start on the next line, like so:
+
+ [^note-id]:
+ Text of the note.
+ """
+ less_than_tab = self.tab_width - 1
+ footnote_def_re = re.compile(r'''
+ ^[ ]{0,%d}\[\^(.+)\]: # id = \1
+ [ \t]*
+ ( # footnote text = \2
+ # First line need not start with the spaces.
+ (?:\s*.*\n+)
+ (?:
+ (?:[ ]{%d} | \t) # Subsequent lines must be indented.
+ .*\n+
+ )*
+ )
+ # Lookahead for non-space at line-start, or end of doc.
+ (?:(?=^[ ]{0,%d}\S)|\Z)
+ ''' % (less_than_tab, self.tab_width, self.tab_width),
+ re.X | re.M)
+ return footnote_def_re.sub(self._extract_footnote_def_sub, text)
+
+
+ _hr_res = [
+ re.compile(r"^[ ]{0,2}([ ]?\*[ ]?){3,}[ \t]*$", re.M),
+ re.compile(r"^[ ]{0,2}([ ]?\-[ ]?){3,}[ \t]*$", re.M),
+ re.compile(r"^[ ]{0,2}([ ]?\_[ ]?){3,}[ \t]*$", re.M),
+ ]
+
+ def _run_block_gamut(self, text):
+ # These are all the transformations that form block-level
+ # tags like paragraphs, headers, and list items.
+
+ text = self._do_headers(text)
+
+ # Do Horizontal Rules:
+ hr = "\n<hr"+self.empty_element_suffix+"\n"
+ for hr_re in self._hr_res:
+ text = hr_re.sub(hr, text)
+
+ text = self._do_lists(text)
+
+ if "pyshell" in self.extras:
+ text = self._prepare_pyshell_blocks(text)
+
+ text = self._do_code_blocks(text)
+
+ text = self._do_block_quotes(text)
+
+ # We already ran _HashHTMLBlocks() before, in Markdown(), but that
+ # was to escape raw HTML in the original Markdown source. This time,
+ # we're escaping the markup we've just created, so that we don't wrap
+ # <p> tags around block-level tags.
+ text = self._hash_html_blocks(text)
+
+ text = self._form_paragraphs(text)
+
+ return text
+
+ def _pyshell_block_sub(self, match):
+ lines = match.group(0).splitlines(0)
+ _dedentlines(lines)
+ indent = ' ' * self.tab_width
+ s = ('\n' # separate from possible cuddled paragraph
+ + indent + ('\n'+indent).join(lines)
+ + '\n\n')
+ return s
+
+ def _prepare_pyshell_blocks(self, text):
+ """Ensure that Python interactive shell sessions are put in
+ code blocks -- even if not properly indented.
+ """
+ if ">>>" not in text:
+ return text
+
+ less_than_tab = self.tab_width - 1
+ _pyshell_block_re = re.compile(r"""
+ ^([ ]{0,%d})>>>[ ].*\n # first line
+ ^(\1.*\S+.*\n)* # any number of subsequent lines
+ ^\n # ends with a blank line
+ """ % less_than_tab, re.M | re.X)
+
+ return _pyshell_block_re.sub(self._pyshell_block_sub, text)
+
+ def _run_span_gamut(self, text):
+ # These are all the transformations that occur *within* block-level
+ # tags like paragraphs, headers, and list items.
+
+ text = self._do_code_spans(text)
+
+ text = self._escape_special_chars(text)
+
+ # Process anchor and image tags.
+ text = self._do_links(text)
+
+ # Make links out of things like `<http://example.com/>`
+ # Must come after _do_links(), because you can use < and >
+ # delimiters in inline links like [this](<url>).
+ text = self._do_auto_links(text)
+
+ if "link-patterns" in self.extras:
+ text = self._do_link_patterns(text)
+
+ text = self._encode_amps_and_angles(text)
+
+ text = self._do_italics_and_bold(text)
+
+ # Do hard breaks:
+ text = re.sub(r" {2,}\n", " <br%s\n" % self.empty_element_suffix, text)
+
+ return text
+
+ # "Sorta" because auto-links are identified as "tag" tokens.
+ _sorta_html_tokenize_re = re.compile(r"""
+ (
+ # tag
+ </?
+ (?:\w+) # tag name
+ (?:\s+(?:[\w-]+:)?[\w-]+=(?:".*?"|'.*?'))* # attributes
+ \s*/?>
+ |
+ # auto-link (e.g., <http://www.activestate.com/>)
+ <\w+[^>]*>
+ |
+ <!--.*?--> # comment
+ |
+ <\?.*?\?> # processing instruction
+ )
+ """, re.X)
+
+ def _escape_special_chars(self, text):
+ # Python markdown note: the HTML tokenization here differs from
+ # that in Markdown.pl, hence the behaviour for subtle cases can
+ # differ (I believe the tokenizer here does a better job because
+ # it isn't susceptible to unmatched '<' and '>' in HTML tags).
+ # Note, however, that '>' is not allowed in an auto-link URL
+ # here.
+ escaped = []
+ is_html_markup = False
+ for token in self._sorta_html_tokenize_re.split(text):
+ if is_html_markup:
+ # Within tags/HTML-comments/auto-links, encode * and _
+ # so they don't conflict with their use in Markdown for
+ # italics and strong. We're replacing each such
+ # character with its corresponding MD5 checksum value;
+ # this is likely overkill, but it should prevent us from
+ # colliding with the escape values by accident.
+ escaped.append(token.replace('*', g_escape_table['*'])
+ .replace('_', g_escape_table['_']))
+ else:
+ escaped.append(self._encode_backslash_escapes(token))
+ is_html_markup = not is_html_markup
+ return ''.join(escaped)
+
+ def _hash_html_spans(self, text):
+ # Used for safe_mode.
+
+ def _is_auto_link(s):
+ if ':' in s and self._auto_link_re.match(s):
+ return True
+ elif '@' in s and self._auto_email_link_re.match(s):
+ return True
+ return False
+
+ tokens = []
+ is_html_markup = False
+ for token in self._sorta_html_tokenize_re.split(text):
+ if is_html_markup and not _is_auto_link(token):
+ sanitized = self._sanitize_html(token)
+ key = _hash_text(sanitized)
+ self.html_spans[key] = sanitized
+ tokens.append(key)
+ else:
+ tokens.append(token)
+ is_html_markup = not is_html_markup
+ return ''.join(tokens)
+
+ def _unhash_html_spans(self, text):
+ for key, sanitized in self.html_spans.items():
+ text = text.replace(key, sanitized)
+ return text
+
+ def _sanitize_html(self, s):
+ if self.safe_mode == "replace":
+ return self.html_removed_text
+ elif self.safe_mode == "escape":
+ replacements = [
+ ('&', '&amp;'),
+ ('<', '&lt;'),
+ ('>', '&gt;'),
+ ]
+ for before, after in replacements:
+ s = s.replace(before, after)
+ return s
+ else:
+ raise MarkdownError("invalid value for 'safe_mode': %r (must be "
+ "'escape' or 'replace')" % self.safe_mode)
+
+ _tail_of_inline_link_re = re.compile(r'''
+ # Match tail of: [text](/url/) or [text](/url/ "title")
+ \( # literal paren
+ [ \t]*
+ (?P<url> # \1
+ <.*?>
+ |
+ .*?
+ )
+ [ \t]*
+ ( # \2
+ (['"]) # quote char = \3
+ (?P<title>.*?)
+ \3 # matching quote
+ )? # title is optional
+ \)
+ ''', re.X | re.S)
+ _tail_of_reference_link_re = re.compile(r'''
+ # Match tail of: [text][id]
+ [ ]? # one optional space
+ (?:\n[ ]*)? # one optional newline followed by spaces
+ \[
+ (?P<id>.*?)
+ \]
+ ''', re.X | re.S)
+
+ def _do_links(self, text):
+ """Turn Markdown link shortcuts into XHTML <a> and <img> tags.
+
+ This is a combination of Markdown.pl's _DoAnchors() and
+ _DoImages(). They are done together because that simplified the
+ approach. It was necessary to use a different approach than
+ Markdown.pl because of the lack of atomic matching support in
+ Python's regex engine used in $g_nested_brackets.
+ """
+ MAX_LINK_TEXT_SENTINEL = 3000 # markdown2 issue 24
+
+ # `anchor_allowed_pos` is used to support img links inside
+ # anchors, but not anchors inside anchors. An anchor's start
+ # pos must be `>= anchor_allowed_pos`.
+ anchor_allowed_pos = 0
+
+ curr_pos = 0
+ while True: # Handle the next link.
+ # The next '[' is the start of:
+ # - an inline anchor: [text](url "title")
+ # - a reference anchor: [text][id]
+ # - an inline img: ![text](url "title")
+ # - a reference img: ![text][id]
+ # - a footnote ref: [^id]
+ # (Only if 'footnotes' extra enabled)
+ # - a footnote defn: [^id]: ...
+ # (Only if 'footnotes' extra enabled) These have already
+ # been stripped in _strip_footnote_definitions() so no
+ # need to watch for them.
+ # - a link definition: [id]: url "title"
+ # These have already been stripped in
+ # _strip_link_definitions() so no need to watch for them.
+ # - not markup: [...anything else...
+ try:
+ start_idx = text.index('[', curr_pos)
+ except ValueError:
+ break
+ text_length = len(text)
+
+ # Find the matching closing ']'.
+ # Markdown.pl allows *matching* brackets in link text so we
+ # will here too. Markdown.pl *doesn't* currently allow
+ # matching brackets in img alt text -- we'll differ in that
+ # regard.
+ bracket_depth = 0
+ for p in range(start_idx+1, min(start_idx+MAX_LINK_TEXT_SENTINEL,
+ text_length)):
+ ch = text[p]
+ if ch == ']':
+ bracket_depth -= 1
+ if bracket_depth < 0:
+ break
+ elif ch == '[':
+ bracket_depth += 1
+ else:
+ # Closing bracket not found within sentinel length.
+ # This isn't markup.
+ curr_pos = start_idx + 1
+ continue
+ link_text = text[start_idx+1:p]
+
+ # Possibly a footnote ref?
+ if "footnotes" in self.extras and link_text.startswith("^"):
+ normed_id = re.sub(r'\W', '-', link_text[1:])
+ if normed_id in self.footnotes:
+ self.footnote_ids.append(normed_id)
+ result = '<sup class="footnote-ref" id="fnref-%s">' \
+ '<a href="#fn-%s">%s</a></sup>' \
+ % (normed_id, normed_id, len(self.footnote_ids))
+ text = text[:start_idx] + result + text[p+1:]
+ else:
+ # This id isn't defined, leave the markup alone.
+ curr_pos = p+1
+ continue
+
+ # Now determine what this is by the remainder.
+ p += 1
+ if p == text_length:
+ return text
+
+ # Inline anchor or img?
+ if text[p] == '(': # attempt at perf improvement
+ match = self._tail_of_inline_link_re.match(text, p)
+ if match:
+ # Handle an inline anchor or img.
+ is_img = start_idx > 0 and text[start_idx-1] == "!"
+ if is_img:
+ start_idx -= 1
+
+ url, title = match.group("url"), match.group("title")
+ if url and url[0] == '<':
+ url = url[1:-1] # '<url>' -> 'url'
+ # We've got to encode these to avoid conflicting
+ # with italics/bold.
+ url = url.replace('*', g_escape_table['*']) \
+ .replace('_', g_escape_table['_'])
+ if title:
+ title_str = ' title="%s"' \
+ % title.replace('*', g_escape_table['*']) \
+ .replace('_', g_escape_table['_']) \
+ .replace('"', '&quot;')
+ else:
+ title_str = ''
+ if is_img:
+ result = '<img src="%s" alt="%s"%s%s' \
+ % (url, link_text.replace('"', '&quot;'),
+ title_str, self.empty_element_suffix)
+ curr_pos = start_idx + len(result)
+ text = text[:start_idx] + result + text[match.end():]
+ elif start_idx >= anchor_allowed_pos:
+ result_head = '<a href="%s"%s>' % (url, title_str)
+ result = '%s%s</a>' % (result_head, link_text)
+ # <img> allowed from curr_pos on, <a> from
+ # anchor_allowed_pos on.
+ curr_pos = start_idx + len(result_head)
+ anchor_allowed_pos = start_idx + len(result)
+ text = text[:start_idx] + result + text[match.end():]
+ else:
+ # Anchor not allowed here.
+ curr_pos = start_idx + 1
+ continue
+
+ # Reference anchor or img?
+ else:
+ match = self._tail_of_reference_link_re.match(text, p)
+ if match:
+ # Handle a reference-style anchor or img.
+ is_img = start_idx > 0 and text[start_idx-1] == "!"
+ if is_img:
+ start_idx -= 1
+ link_id = match.group("id").lower()
+ if not link_id:
+ link_id = link_text.lower() # for links like [this][]
+ if link_id in self.urls:
+ url = self.urls[link_id]
+ # We've got to encode these to avoid conflicting
+ # with italics/bold.
+ url = url.replace('*', g_escape_table['*']) \
+ .replace('_', g_escape_table['_'])
+ title = self.titles.get(link_id)
+ if title:
+ title = title.replace('*', g_escape_table['*']) \
+ .replace('_', g_escape_table['_'])
+ title_str = ' title="%s"' % title
+ else:
+ title_str = ''
+ if is_img:
+ result = '<img src="%s" alt="%s"%s%s' \
+ % (url, link_text.replace('"', '&quot;'),
+ title_str, self.empty_element_suffix)
+ curr_pos = start_idx + len(result)
+ text = text[:start_idx] + result + text[match.end():]
+ elif start_idx >= anchor_allowed_pos:
+ result = '<a href="%s"%s>%s</a>' \
+ % (url, title_str, link_text)
+ result_head = '<a href="%s"%s>' % (url, title_str)
+ result = '%s%s</a>' % (result_head, link_text)
+ # <img> allowed from curr_pos on, <a> from
+ # anchor_allowed_pos on.
+ curr_pos = start_idx + len(result_head)
+ anchor_allowed_pos = start_idx + len(result)
+ text = text[:start_idx] + result + text[match.end():]
+ else:
+ # Anchor not allowed here.
+ curr_pos = start_idx + 1
+ else:
+ # This id isn't defined, leave the markup alone.
+ curr_pos = match.end()
+ continue
+
+ # Otherwise, it isn't markup.
+ curr_pos = start_idx + 1
+
+ return text
+
+
+ _setext_h_re = re.compile(r'^(.+)[ \t]*\n(=+|-+)[ \t]*\n+', re.M)
+ def _setext_h_sub(self, match):
+ n = {"=": 1, "-": 2}[match.group(2)[0]]
+ demote_headers = self.extras.get("demote-headers")
+ if demote_headers:
+ n = min(n + demote_headers, 6)
+ return "<h%d>%s</h%d>\n\n" \
+ % (n, self._run_span_gamut(match.group(1)), n)
+
+ _atx_h_re = re.compile(r'''
+ ^(\#{1,6}) # \1 = string of #'s
+ [ \t]*
+ (.+?) # \2 = Header text
+ [ \t]*
+ (?<!\\) # ensure not an escaped trailing '#'
+ \#* # optional closing #'s (not counted)
+ \n+
+ ''', re.X | re.M)
+ def _atx_h_sub(self, match):
+ n = len(match.group(1))
+ demote_headers = self.extras.get("demote-headers")
+ if demote_headers:
+ n = min(n + demote_headers, 6)
+ return "<h%d>%s</h%d>\n\n" \
+ % (n, self._run_span_gamut(match.group(2)), n)
+
+ def _do_headers(self, text):
+ # Setext-style headers:
+ # Header 1
+ # ========
+ #
+ # Header 2
+ # --------
+ text = self._setext_h_re.sub(self._setext_h_sub, text)
+
+ # atx-style headers:
+ # # Header 1
+ # ## Header 2
+ # ## Header 2 with closing hashes ##
+ # ...
+ # ###### Header 6
+ text = self._atx_h_re.sub(self._atx_h_sub, text)
+
+ return text
+
+
+ _marker_ul_chars = '*+-'
+ _marker_any = r'(?:[%s]|\d+\.)' % _marker_ul_chars
+ _marker_ul = '(?:[%s])' % _marker_ul_chars
+ _marker_ol = r'(?:\d+\.)'
+
+ def _list_sub(self, match):
+ lst = match.group(1)
+ lst_type = match.group(3) in self._marker_ul_chars and "ul" or "ol"
+ result = self._process_list_items(lst)
+ if self.list_level:
+ return "<%s>\n%s</%s>\n" % (lst_type, result, lst_type)
+ else:
+ return "<%s>\n%s</%s>\n\n" % (lst_type, result, lst_type)
+
+ def _do_lists(self, text):
+ # Form HTML ordered (numbered) and unordered (bulleted) lists.
+
+ for marker_pat in (self._marker_ul, self._marker_ol):
+ # Re-usable pattern to match any entire ul or ol list:
+ less_than_tab = self.tab_width - 1
+ whole_list = r'''
+ ( # \1 = whole list
+ ( # \2
+ [ ]{0,%d}
+ (%s) # \3 = first list item marker
+ [ \t]+
+ )
+ (?:.+?)
+ ( # \4
+ \Z
+ |
+ \n{2,}
+ (?=\S)
+ (?! # Negative lookahead for another list item marker
+ [ \t]*
+ %s[ \t]+
+ )
+ )
+ )
+ ''' % (less_than_tab, marker_pat, marker_pat)
+
+ # We use a different prefix before nested lists than top-level lists.
+ # See extended comment in _process_list_items().
+ #
+ # Note: There's a bit of duplication here. My original implementation
+ # created a scalar regex pattern as the conditional result of the test on
+ # $g_list_level, and then only ran the $text =~ s{...}{...}egmx
+ # substitution once, using the scalar as the pattern. This worked,
+ # everywhere except when running under MT on my hosting account at Pair
+ # Networks. There, this caused all rebuilds to be killed by the reaper (or
+ # perhaps they crashed, but that seems incredibly unlikely given that the
+ # same script on the same server ran fine *except* under MT. I've spent
+ # more time trying to figure out why this is happening than I'd like to
+ # admit. My only guess, backed up by the fact that this workaround works,
+ # is that Perl optimizes the substition when it can figure out that the
+ # pattern will never change, and when this optimization isn't on, we run
+ # afoul of the reaper. Thus, the slightly redundant code to that uses two
+ # static s/// patterns rather than one conditional pattern.
+
+ if self.list_level:
+ sub_list_re = re.compile("^"+whole_list, re.X | re.M | re.S)
+ text = sub_list_re.sub(self._list_sub, text)
+ else:
+ list_re = re.compile(r"(?:(?<=\n\n)|\A\n?)"+whole_list,
+ re.X | re.M | re.S)
+ text = list_re.sub(self._list_sub, text)
+
+ return text
+
+ _list_item_re = re.compile(r'''
+ (\n)? # leading line = \1
+ (^[ \t]*) # leading whitespace = \2
+ (%s) [ \t]+ # list marker = \3
+ ((?:.+?) # list item text = \4
+ (\n{1,2})) # eols = \5
+ (?= \n* (\Z | \2 (%s) [ \t]+))
+ ''' % (_marker_any, _marker_any),
+ re.M | re.X | re.S)
+
+ _last_li_endswith_two_eols = False
+ def _list_item_sub(self, match):
+ item = match.group(4)
+ leading_line = match.group(1)
+ leading_space = match.group(2)
+ if leading_line or "\n\n" in item or self._last_li_endswith_two_eols:
+ item = self._run_block_gamut(self._outdent(item))
+ else:
+ # Recursion for sub-lists:
+ item = self._do_lists(self._outdent(item))
+ if item.endswith('\n'):
+ item = item[:-1]
+ item = self._run_span_gamut(item)
+ self._last_li_endswith_two_eols = (len(match.group(5)) == 2)
+ return "<li>%s</li>\n" % item
+
+ def _process_list_items(self, list_str):
+ # Process the contents of a single ordered or unordered list,
+ # splitting it into individual list items.
+
+ # The $g_list_level global keeps track of when we're inside a list.
+ # Each time we enter a list, we increment it; when we leave a list,
+ # we decrement. If it's zero, we're not in a list anymore.
+ #
+ # We do this because when we're not inside a list, we want to treat
+ # something like this:
+ #
+ # I recommend upgrading to version
+ # 8. Oops, now this line is treated
+ # as a sub-list.
+ #
+ # As a single paragraph, despite the fact that the second line starts
+ # with a digit-period-space sequence.
+ #
+ # Whereas when we're inside a list (or sub-list), that line will be
+ # treated as the start of a sub-list. What a kludge, huh? This is
+ # an aspect of Markdown's syntax that's hard to parse perfectly
+ # without resorting to mind-reading. Perhaps the solution is to
+ # change the syntax rules such that sub-lists must start with a
+ # starting cardinal number; e.g. "1." or "a.".
+ self.list_level += 1
+ self._last_li_endswith_two_eols = False
+ list_str = list_str.rstrip('\n') + '\n'
+ list_str = self._list_item_re.sub(self._list_item_sub, list_str)
+ self.list_level -= 1
+ return list_str
+
+ def _get_pygments_lexer(self, lexer_name):
+ try:
+ from pygments import lexers, util
+ except ImportError:
+ return None
+ try:
+ return lexers.get_lexer_by_name(lexer_name)
+ except util.ClassNotFound:
+ return None
+
+ def _color_with_pygments(self, codeblock, lexer, **formatter_opts):
+ import pygments
+ import pygments.formatters
+
+ class HtmlCodeFormatter(pygments.formatters.HtmlFormatter):
+ def _wrap_code(self, inner):
+ """A function for use in a Pygments Formatter which
+ wraps in <code> tags.
+ """
+ yield 0, "<code>"
+ for tup in inner:
+ yield tup
+ yield 0, "</code>"
+
+ def wrap(self, source, outfile):
+ """Return the source with a code, pre, and div."""
+ return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
+
+ formatter = HtmlCodeFormatter(cssclass="codehilite", **formatter_opts)
+ return pygments.highlight(codeblock, lexer, formatter)
+
+ def _code_block_sub(self, match):
+ codeblock = match.group(1)
+ codeblock = self._outdent(codeblock)
+ codeblock = self._detab(codeblock)
+ codeblock = codeblock.lstrip('\n') # trim leading newlines
+ codeblock = codeblock.rstrip() # trim trailing whitespace
+
+ if "code-color" in self.extras and codeblock.startswith(":::"):
+ lexer_name, rest = codeblock.split('\n', 1)
+ lexer_name = lexer_name[3:].strip()
+ lexer = self._get_pygments_lexer(lexer_name)
+ codeblock = rest.lstrip("\n") # Remove lexer declaration line.
+ if lexer:
+ formatter_opts = self.extras['code-color'] or {}
+ colored = self._color_with_pygments(codeblock, lexer,
+ **formatter_opts)
+ return "\n\n%s\n\n" % colored
+
+ codeblock = self._encode_code(codeblock)
+ return "\n\n<pre><code>%s\n</code></pre>\n\n" % codeblock
+
+ def _do_code_blocks(self, text):
+ """Process Markdown `<pre><code>` blocks."""
+ code_block_re = re.compile(r'''
+ (?:\n\n|\A)
+ ( # $1 = the code block -- one or more lines, starting with a space/tab
+ (?:
+ (?:[ ]{%d} | \t) # Lines must start with a tab or a tab-width of spaces
+ .*\n+
+ )+
+ )
+ ((?=^[ ]{0,%d}\S)|\Z) # Lookahead for non-space at line-start, or end of doc
+ ''' % (self.tab_width, self.tab_width),
+ re.M | re.X)
+
+ return code_block_re.sub(self._code_block_sub, text)
+
+
+ # Rules for a code span:
+ # - backslash escapes are not interpreted in a code span
+ # - to include one or or a run of more backticks the delimiters must
+ # be a longer run of backticks
+ # - cannot start or end a code span with a backtick; pad with a
+ # space and that space will be removed in the emitted HTML
+ # See `test/tm-cases/escapes.text` for a number of edge-case
+ # examples.
+ _code_span_re = re.compile(r'''
+ (?<!\\)
+ (`+) # \1 = Opening run of `
+ (?!`) # See Note A test/tm-cases/escapes.text
+ (.+?) # \2 = The code block
+ (?<!`)
+ \1 # Matching closer
+ (?!`)
+ ''', re.X | re.S)
+
+ def _code_span_sub(self, match):
+ c = match.group(2).strip(" \t")
+ c = self._encode_code(c)
+ return "<code>%s</code>" % c
+
+ def _do_code_spans(self, text):
+ # * Backtick quotes are used for <code></code> spans.
+ #
+ # * You can use multiple backticks as the delimiters if you want to
+ # include literal backticks in the code span. So, this input:
+ #
+ # Just type ``foo `bar` baz`` at the prompt.
+ #
+ # Will translate to:
+ #
+ # <p>Just type <code>foo `bar` baz</code> at the prompt.</p>
+ #
+ # There's no arbitrary limit to the number of backticks you
+ # can use as delimters. If you need three consecutive backticks
+ # in your code, use four for delimiters, etc.
+ #
+ # * You can use spaces to get literal backticks at the edges:
+ #
+ # ... type `` `bar` `` ...
+ #
+ # Turns to:
+ #
+ # ... type <code>`bar`</code> ...
+ return self._code_span_re.sub(self._code_span_sub, text)
+
+ def _encode_code(self, text):
+ """Encode/escape certain characters inside Markdown code runs.
+ The point is that in code, these characters are literals,
+ and lose their special Markdown meanings.
+ """
+ replacements = [
+ # Encode all ampersands; HTML entities are not
+ # entities within a Markdown code span.
+ ('&', '&amp;'),
+ # Do the angle bracket song and dance:
+ ('<', '&lt;'),
+ ('>', '&gt;'),
+ # Now, escape characters that are magic in Markdown:
+ ('*', g_escape_table['*']),
+ ('_', g_escape_table['_']),
+ ('{', g_escape_table['{']),
+ ('}', g_escape_table['}']),
+ ('[', g_escape_table['[']),
+ (']', g_escape_table[']']),
+ ('\\', g_escape_table['\\']),
+ ]
+ for before, after in replacements:
+ text = text.replace(before, after)
+ return text
+
+ _strong_re = re.compile(r"(\*\*|__)(?=\S)(.+?[*_]*)(?<=\S)\1", re.S)
+ _em_re = re.compile(r"(\*|_)(?=\S)(.+?)(?<=\S)\1", re.S)
+ _code_friendly_strong_re = re.compile(r"\*\*(?=\S)(.+?[*_]*)(?<=\S)\*\*", re.S)
+ _code_friendly_em_re = re.compile(r"\*(?=\S)(.+?)(?<=\S)\*", re.S)
+ def _do_italics_and_bold(self, text):
+ # <strong> must go first:
+ if "code-friendly" in self.extras:
+ text = self._code_friendly_strong_re.sub(r"<strong>\1</strong>", text)
+ text = self._code_friendly_em_re.sub(r"<em>\1</em>", text)
+ else:
+ text = self._strong_re.sub(r"<strong>\2</strong>", text)
+ text = self._em_re.sub(r"<em>\2</em>", text)
+ return text
+
+
+ _block_quote_re = re.compile(r'''
+ ( # Wrap whole match in \1
+ (
+ ^[ \t]*>[ \t]? # '>' at the start of a line
+ .+\n # rest of the first line
+ (.+\n)* # subsequent consecutive lines
+ \n* # blanks
+ )+
+ )
+ ''', re.M | re.X)
+ _bq_one_level_re = re.compile('^[ \t]*>[ \t]?', re.M);
+
+ _html_pre_block_re = re.compile(r'(\s*<pre>.+?</pre>)', re.S)
+ def _dedent_two_spaces_sub(self, match):
+ return re.sub(r'(?m)^ ', '', match.group(1))
+
+ def _block_quote_sub(self, match):
+ bq = match.group(1)
+ bq = self._bq_one_level_re.sub('', bq) # trim one level of quoting
+ bq = self._ws_only_line_re.sub('', bq) # trim whitespace-only lines
+ bq = self._run_block_gamut(bq) # recurse
+
+ bq = re.sub('(?m)^', ' ', bq)
+ # These leading spaces screw with <pre> content, so we need to fix that:
+ bq = self._html_pre_block_re.sub(self._dedent_two_spaces_sub, bq)
+
+ return "<blockquote>\n%s\n</blockquote>\n\n" % bq
+
+ def _do_block_quotes(self, text):
+ if '>' not in text:
+ return text
+ return self._block_quote_re.sub(self._block_quote_sub, text)
+
+ def _form_paragraphs(self, text):
+ # Strip leading and trailing lines:
+ text = text.strip('\n')
+
+ # Wrap <p> tags.
+ grafs = re.split(r"\n{2,}", text)
+ for i, graf in enumerate(grafs):
+ if graf in self.html_blocks:
+ # Unhashify HTML blocks
+ grafs[i] = self.html_blocks[graf]
+ else:
+ # Wrap <p> tags.
+ graf = self._run_span_gamut(graf)
+ grafs[i] = "<p>" + graf.lstrip(" \t") + "</p>"
+
+ return "\n\n".join(grafs)
+
+ def _add_footnotes(self, text):
+ if self.footnotes:
+ footer = [
+ '<div class="footnotes">',
+ '<hr' + self.empty_element_suffix,
+ '<ol>',
+ ]
+ for i, id in enumerate(self.footnote_ids):
+ if i != 0:
+ footer.append('')
+ footer.append('<li id="fn-%s">' % id)
+ footer.append(self._run_block_gamut(self.footnotes[id]))
+ backlink = ('<a href="#fnref-%s" '
+ 'class="footnoteBackLink" '
+ 'title="Jump back to footnote %d in the text.">'
+ '&#8617;</a>' % (id, i+1))
+ if footer[-1].endswith("</p>"):
+ footer[-1] = footer[-1][:-len("</p>")] \
+ + '&nbsp;' + backlink + "</p>"
+ else:
+ footer.append("\n<p>%s</p>" % backlink)
+ footer.append('</li>')
+ footer.append('</ol>')
+ footer.append('</div>')
+ return text + '\n\n' + '\n'.join(footer)
+ else:
+ return text
+
+ # Ampersand-encoding based entirely on Nat Irons's Amputator MT plugin:
+ # http://bumppo.net/projects/amputator/
+ _ampersand_re = re.compile(r'&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)')
+ _naked_lt_re = re.compile(r'<(?![a-z/?\$!])', re.I)
+ _naked_gt_re = re.compile(r'''(?<![a-z?!/'"-])>''', re.I)
+
+ def _encode_amps_and_angles(self, text):
+ # Smart processing for ampersands and angle brackets that need
+ # to be encoded.
+ text = self._ampersand_re.sub('&amp;', text)
+
+ # Encode naked <'s
+ text = self._naked_lt_re.sub('&lt;', text)
+
+ # Encode naked >'s
+ # Note: Other markdown implementations (e.g. Markdown.pl, PHP
+ # Markdown) don't do this.
+ text = self._naked_gt_re.sub('&gt;', text)
+ return text
+
+ def _encode_backslash_escapes(self, text):
+ for ch, escape in g_escape_table.items():
+ text = text.replace("\\"+ch, escape)
+ return text
+
+ _auto_link_re = re.compile(r'<((https?|ftp):[^\'">\s]+)>', re.I)
+ def _auto_link_sub(self, match):
+ g1 = match.group(1)
+ return '<a href="%s">%s</a>' % (g1, g1)
+
+ _auto_email_link_re = re.compile(r"""
+ <
+ (?:mailto:)?
+ (
+ [-.\w]+
+ \@
+ [-\w]+(\.[-\w]+)*\.[a-z]+
+ )
+ >
+ """, re.I | re.X | re.U)
+ def _auto_email_link_sub(self, match):
+ return self._encode_email_address(
+ self._unescape_special_chars(match.group(1)))
+
+ def _do_auto_links(self, text):
+ text = self._auto_link_re.sub(self._auto_link_sub, text)
+ text = self._auto_email_link_re.sub(self._auto_email_link_sub, text)
+ return text
+
+ def _encode_email_address(self, addr):
+ # Input: an email address, e.g. "foo@example.com"
+ #
+ # Output: the email address as a mailto link, with each character
+ # of the address encoded as either a decimal or hex entity, in
+ # the hopes of foiling most address harvesting spam bots. E.g.:
+ #
+ # <a href="&#x6D;&#97;&#105;&#108;&#x74;&#111;:&#102;&#111;&#111;&#64;&#101;
+ # x&#x61;&#109;&#x70;&#108;&#x65;&#x2E;&#99;&#111;&#109;">&#102;&#111;&#111;
+ # &#64;&#101;x&#x61;&#109;&#x70;&#108;&#x65;&#x2E;&#99;&#111;&#109;</a>
+ #
+ # Based on a filter by Matthew Wickline, posted to the BBEdit-Talk
+ # mailing list: <http://tinyurl.com/yu7ue>
+ chars = [_xml_encode_email_char_at_random(ch)
+ for ch in "mailto:" + addr]
+ # Strip the mailto: from the visible part.
+ addr = '<a href="%s">%s</a>' \
+ % (''.join(chars), ''.join(chars[7:]))
+ return addr
+
+ def _do_link_patterns(self, text):
+ """Caveat emptor: there isn't much guarding against link
+ patterns being formed inside other standard Markdown links, e.g.
+ inside a [link def][like this].
+
+ Dev Notes: *Could* consider prefixing regexes with a negative
+ lookbehind assertion to attempt to guard against this.
+ """
+ link_from_hash = {}
+ for regex, repl in self.link_patterns:
+ replacements = []
+ for match in regex.finditer(text):
+ if hasattr(repl, "__call__"):
+ href = repl(match)
+ else:
+ href = match.expand(repl)
+ replacements.append((match.span(), href))
+ for (start, end), href in reversed(replacements):
+ escaped_href = (
+ href.replace('"', '&quot;') # b/c of attr quote
+ # To avoid markdown <em> and <strong>:
+ .replace('*', g_escape_table['*'])
+ .replace('_', g_escape_table['_']))
+ link = '<a href="%s">%s</a>' % (escaped_href, text[start:end])
+ hash = md5(link).hexdigest()
+ link_from_hash[hash] = link
+ text = text[:start] + hash + text[end:]
+ for hash, link in link_from_hash.items():
+ text = text.replace(hash, link)
+ return text
+
+ def _unescape_special_chars(self, text):
+ # Swap back in all the special characters we've hidden.
+ for ch, hash in g_escape_table.items():
+ text = text.replace(hash, ch)
+ return text
+
+ def _outdent(self, text):
+ # Remove one level of line-leading tabs or spaces
+ return self._outdent_re.sub('', text)
+
+
+class MarkdownWithExtras(Markdown):
+ """A markdowner class that enables most extras:
+
+ - footnotes
+ - code-color (only has effect if 'pygments' Python module on path)
+
+ These are not included:
+ - pyshell (specific to Python-related documenting)
+ - code-friendly (because it *disables* part of the syntax)
+ - link-patterns (because you need to specify some actual
+ link-patterns anyway)
+ """
+ extras = ["footnotes", "code-color"]
+
+
+#---- internal support functions
+
+# From http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52549
+def _curry(*args, **kwargs):
+ function, args = args[0], args[1:]
+ def result(*rest, **kwrest):
+ combined = kwargs.copy()
+ combined.update(kwrest)
+ return function(*args + rest, **combined)
+ return result
+
+# Recipe: regex_from_encoded_pattern (1.0)
+def _regex_from_encoded_pattern(s):
+ """'foo' -> re.compile(re.escape('foo'))
+ '/foo/' -> re.compile('foo')
+ '/foo/i' -> re.compile('foo', re.I)
+ """
+ if s.startswith('/') and s.rfind('/') != 0:
+ # Parse it: /PATTERN/FLAGS
+ idx = s.rfind('/')
+ pattern, flags_str = s[1:idx], s[idx+1:]
+ flag_from_char = {
+ "i": re.IGNORECASE,
+ "l": re.LOCALE,
+ "s": re.DOTALL,
+ "m": re.MULTILINE,
+ "u": re.UNICODE,
+ }
+ flags = 0
+ for char in flags_str:
+ try:
+ flags |= flag_from_char[char]
+ except KeyError:
+ raise ValueError("unsupported regex flag: '%s' in '%s' "
+ "(must be one of '%s')"
+ % (char, s, ''.join(flag_from_char.keys())))
+ return re.compile(s[1:idx], flags)
+ else: # not an encoded regex
+ return re.compile(re.escape(s))
+
+# Recipe: dedent (0.1.2)
+def _dedentlines(lines, tabsize=8, skip_first_line=False):
+ """_dedentlines(lines, tabsize=8, skip_first_line=False) -> dedented lines
+
+ "lines" is a list of lines to dedent.
+ "tabsize" is the tab width to use for indent width calculations.
+ "skip_first_line" is a boolean indicating if the first line should
+ be skipped for calculating the indent width and for dedenting.
+ This is sometimes useful for docstrings and similar.
+
+ Same as dedent() except operates on a sequence of lines. Note: the
+ lines list is modified **in-place**.
+ """
+ DEBUG = False
+ if DEBUG:
+ print "dedent: dedent(..., tabsize=%d, skip_first_line=%r)"\
+ % (tabsize, skip_first_line)
+ indents = []
+ margin = None
+ for i, line in enumerate(lines):
+ if i == 0 and skip_first_line: continue
+ indent = 0
+ for ch in line:
+ if ch == ' ':
+ indent += 1
+ elif ch == '\t':
+ indent += tabsize - (indent % tabsize)
+ elif ch in '\r\n':
+ continue # skip all-whitespace lines
+ else:
+ break
+ else:
+ continue # skip all-whitespace lines
+ if DEBUG: print "dedent: indent=%d: %r" % (indent, line)
+ if margin is None:
+ margin = indent
+ else:
+ margin = min(margin, indent)
+ if DEBUG: print "dedent: margin=%r" % margin
+
+ if margin is not None and margin > 0:
+ for i, line in enumerate(lines):
+ if i == 0 and skip_first_line: continue
+ removed = 0
+ for j, ch in enumerate(line):
+ if ch == ' ':
+ removed += 1
+ elif ch == '\t':
+ removed += tabsize - (removed % tabsize)
+ elif ch in '\r\n':
+ if DEBUG: print "dedent: %r: EOL -> strip up to EOL" % line
+ lines[i] = lines[i][j:]
+ break
+ else:
+ raise ValueError("unexpected non-whitespace char %r in "
+ "line %r while removing %d-space margin"
+ % (ch, line, margin))
+ if DEBUG:
+ print "dedent: %r: %r -> removed %d/%d"\
+ % (line, ch, removed, margin)
+ if removed == margin:
+ lines[i] = lines[i][j+1:]
+ break
+ elif removed > margin:
+ lines[i] = ' '*(removed-margin) + lines[i][j+1:]
+ break
+ else:
+ if removed:
+ lines[i] = lines[i][removed:]
+ return lines
+
+def _dedent(text, tabsize=8, skip_first_line=False):
+ """_dedent(text, tabsize=8, skip_first_line=False) -> dedented text
+
+ "text" is the text to dedent.
+ "tabsize" is the tab width to use for indent width calculations.
+ "skip_first_line" is a boolean indicating if the first line should
+ be skipped for calculating the indent width and for dedenting.
+ This is sometimes useful for docstrings and similar.
+
+ textwrap.dedent(s), but don't expand tabs to spaces
+ """
+ lines = text.splitlines(1)
+ _dedentlines(lines, tabsize=tabsize, skip_first_line=skip_first_line)
+ return ''.join(lines)
+
+
+class _memoized(object):
+ """Decorator that caches a function's return value each time it is called.
+ If called later with the same arguments, the cached value is returned, and
+ not re-evaluated.
+
+ http://wiki.python.org/moin/PythonDecoratorLibrary
+ """
+ def __init__(self, func):
+ self.func = func
+ self.cache = {}
+ def __call__(self, *args):
+ try:
+ return self.cache[args]
+ except KeyError:
+ self.cache[args] = value = self.func(*args)
+ return value
+ except TypeError:
+ # uncachable -- for instance, passing a list as an argument.
+ # Better to not cache than to blow up entirely.
+ return self.func(*args)
+ def __repr__(self):
+ """Return the function's docstring."""
+ return self.func.__doc__
+
+
+def _xml_oneliner_re_from_tab_width(tab_width):
+ """Standalone XML processing instruction regex."""
+ return re.compile(r"""
+ (?:
+ (?<=\n\n) # Starting after a blank line
+ | # or
+ \A\n? # the beginning of the doc
+ )
+ ( # save in $1
+ [ ]{0,%d}
+ (?:
+ <\?\w+\b\s+.*?\?> # XML processing instruction
+ |
+ <\w+:\w+\b\s+.*?/> # namespaced single tag
+ )
+ [ \t]*
+ (?=\n{2,}|\Z) # followed by a blank line or end of document
+ )
+ """ % (tab_width - 1), re.X)
+_xml_oneliner_re_from_tab_width = _memoized(_xml_oneliner_re_from_tab_width)
+
+def _hr_tag_re_from_tab_width(tab_width):
+ return re.compile(r"""
+ (?:
+ (?<=\n\n) # Starting after a blank line
+ | # or
+ \A\n? # the beginning of the doc
+ )
+ ( # save in \1
+ [ ]{0,%d}
+ <(hr) # start tag = \2
+ \b # word break
+ ([^<>])*? #
+ /?> # the matching end tag
+ [ \t]*
+ (?=\n{2,}|\Z) # followed by a blank line or end of document
+ )
+ """ % (tab_width - 1), re.X)
+_hr_tag_re_from_tab_width = _memoized(_hr_tag_re_from_tab_width)
+
+
+def _xml_encode_email_char_at_random(ch):
+ r = random()
+ # Roughly 10% raw, 45% hex, 45% dec.
+ # '@' *must* be encoded. I [John Gruber] insist.
+ # Issue 26: '_' must be encoded.
+ if r > 0.9 and ch not in "@_":
+ return ch
+ elif r < 0.45:
+ # The [1:] is to drop leading '0': 0x63 -> x63
+ return '&#%s;' % hex(ord(ch))[1:]
+ else:
+ return '&#%s;' % ord(ch)
+
+def _hash_text(text):
+ return 'md5:'+md5(text.encode("utf-8")).hexdigest()
+
+
+#---- mainline
+
+class _NoReflowFormatter(optparse.IndentedHelpFormatter):
+ """An optparse formatter that does NOT reflow the description."""
+ def format_description(self, description):
+ return description or ""
+
+def _test():
+ import doctest
+ doctest.testmod()
+
+def main(argv=None):
+ if argv is None:
+ argv = sys.argv
+ if not logging.root.handlers:
+ logging.basicConfig()
+
+ usage = "usage: %prog [PATHS...]"
+ version = "%prog "+__version__
+ parser = optparse.OptionParser(prog="markdown2", usage=usage,
+ version=version, description=cmdln_desc,
+ formatter=_NoReflowFormatter())
+ parser.add_option("-v", "--verbose", dest="log_level",
+ action="store_const", const=logging.DEBUG,
+ help="more verbose output")
+ parser.add_option("--encoding",
+ help="specify encoding of text content")
+ parser.add_option("--html4tags", action="store_true", default=False,
+ help="use HTML 4 style for empty element tags")
+ parser.add_option("-s", "--safe", metavar="MODE", dest="safe_mode",
+ help="sanitize literal HTML: 'escape' escapes "
+ "HTML meta chars, 'replace' replaces with an "
+ "[HTML_REMOVED] note")
+ parser.add_option("-x", "--extras", action="append",
+ help="Turn on specific extra features (not part of "
+ "the core Markdown spec). Supported values: "
+ "'code-friendly' disables _/__ for emphasis; "
+ "'code-color' adds code-block syntax coloring; "
+ "'link-patterns' adds auto-linking based on patterns; "
+ "'footnotes' adds the footnotes syntax;"
+ "'xml' passes one-liner processing instructions and namespaced XML tags;"
+ "'pyshell' to put unindented Python interactive shell sessions in a <code> block.")
+ parser.add_option("--use-file-vars",
+ help="Look for and use Emacs-style 'markdown-extras' "
+ "file var to turn on extras. See "
+ "<http://code.google.com/p/python-markdown2/wiki/Extras>.")
+ parser.add_option("--link-patterns-file",
+ help="path to a link pattern file")
+ parser.add_option("--self-test", action="store_true",
+ help="run internal self-tests (some doctests)")
+ parser.add_option("--compare", action="store_true",
+ help="run against Markdown.pl as well (for testing)")
+ parser.set_defaults(log_level=logging.INFO, compare=False,
+ encoding="utf-8", safe_mode=None, use_file_vars=False)
+ opts, paths = parser.parse_args()
+ log.setLevel(opts.log_level)
+
+ if opts.self_test:
+ return _test()
+
+ if opts.extras:
+ extras = {}
+ for s in opts.extras:
+ splitter = re.compile("[,;: ]+")
+ for e in splitter.split(s):
+ if '=' in e:
+ ename, earg = e.split('=', 1)
+ try:
+ earg = int(earg)
+ except ValueError:
+ pass
+ else:
+ ename, earg = e, None
+ extras[ename] = earg
+ else:
+ extras = None
+
+ if opts.link_patterns_file:
+ link_patterns = []
+ f = open(opts.link_patterns_file)
+ try:
+ for i, line in enumerate(f.readlines()):
+ if not line.strip(): continue
+ if line.lstrip().startswith("#"): continue
+ try:
+ pat, href = line.rstrip().rsplit(None, 1)
+ except ValueError:
+ raise MarkdownError("%s:%d: invalid link pattern line: %r"
+ % (opts.link_patterns_file, i+1, line))
+ link_patterns.append(
+ (_regex_from_encoded_pattern(pat), href))
+ finally:
+ f.close()
+ else:
+ link_patterns = None
+
+ from os.path import join, dirname, abspath, exists
+ markdown_pl = join(dirname(dirname(abspath(__file__))), "test",
+ "Markdown.pl")
+ for path in paths:
+ if opts.compare:
+ print "==== Markdown.pl ===="
+ perl_cmd = 'perl %s "%s"' % (markdown_pl, path)
+ o = os.popen(perl_cmd)
+ perl_html = o.read()
+ o.close()
+ sys.stdout.write(perl_html)
+ print "==== markdown2.py ===="
+ html = markdown_path(path, encoding=opts.encoding,
+ html4tags=opts.html4tags,
+ safe_mode=opts.safe_mode,
+ extras=extras, link_patterns=link_patterns,
+ use_file_vars=opts.use_file_vars)
+ sys.stdout.write(
+ html.encode(sys.stdout.encoding or "utf-8", 'xmlcharrefreplace'))
+ if opts.compare:
+ test_dir = join(dirname(dirname(abspath(__file__))), "test")
+ if exists(join(test_dir, "test_markdown2.py")):
+ sys.path.insert(0, test_dir)
+ from test_markdown2 import norm_html_from_html
+ norm_html = norm_html_from_html(html)
+ norm_perl_html = norm_html_from_html(perl_html)
+ else:
+ norm_html = html
+ norm_perl_html = perl_html
+ print "==== match? %r ====" % (norm_perl_html == norm_html)
+
+
+if __name__ == "__main__":
+ sys.exit( main(sys.argv) )
+
diff --git a/vendor/tornado/demos/blog/schema.sql b/vendor/tornado/demos/blog/schema.sql
new file mode 100644
index 0000000000..86bff9a8ad
--- /dev/null
+++ b/vendor/tornado/demos/blog/schema.sql
@@ -0,0 +1,44 @@
+-- Copyright 2009 FriendFeed
+--
+-- Licensed under the Apache License, Version 2.0 (the "License"); you may
+-- not use this file except in compliance with the License. You may obtain
+-- a copy of the License at
+--
+-- http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+-- WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+-- License for the specific language governing permissions and limitations
+-- under the License.
+
+-- To create the database:
+-- CREATE DATABASE blog;
+-- GRANT ALL PRIVILEGES ON blog.* TO 'blog'@'localhost' IDENTIFIED BY 'blog';
+--
+-- To reload the tables:
+-- mysql --user=blog --password=blog --database=blog < schema.sql
+
+SET SESSION storage_engine = "InnoDB";
+SET SESSION time_zone = "+0:00";
+ALTER DATABASE CHARACTER SET "utf8";
+
+DROP TABLE IF EXISTS entries;
+CREATE TABLE entries (
+ id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
+ author_id INT NOT NULL REFERENCES authors(id),
+ slug VARCHAR(100) NOT NULL UNIQUE,
+ title VARCHAR(512) NOT NULL,
+ markdown MEDIUMTEXT NOT NULL,
+ html MEDIUMTEXT NOT NULL,
+ published DATETIME NOT NULL,
+ updated TIMESTAMP NOT NULL,
+ KEY (published)
+);
+
+DROP TABLE IF EXISTS authors;
+CREATE TABLE authors (
+ id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
+ email VARCHAR(100) NOT NULL UNIQUE,
+ name VARCHAR(100) NOT NULL
+);
diff --git a/vendor/tornado/demos/blog/static/blog.css b/vendor/tornado/demos/blog/static/blog.css
new file mode 100644
index 0000000000..8902ec1f22
--- /dev/null
+++ b/vendor/tornado/demos/blog/static/blog.css
@@ -0,0 +1,153 @@
+/*
+ * Copyright 2009 Facebook
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License. You may obtain
+ * a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+body {
+ background: white;
+ color: black;
+ margin: 15px;
+ margin-top: 0;
+}
+
+body,
+input,
+textarea {
+ font-family: Georgia, serif;
+ font-size: 12pt;
+}
+
+table {
+ border-collapse: collapse;
+ border: 0;
+}
+
+td {
+ border: 0;
+ padding: 0;
+}
+
+h1,
+h2,
+h3,
+h4 {
+ font-family: "Helvetica Nue", Helvetica, Arial, sans-serif;
+ margin: 0;
+}
+
+h1 {
+ font-size: 20pt;
+}
+
+pre,
+code {
+ font-family: monospace;
+ color: #060;
+}
+
+pre {
+ margin-left: 1em;
+ padding-left: 1em;
+ border-left: 1px solid silver;
+ line-height: 14pt;
+}
+
+a,
+a code {
+ color: #00c;
+}
+
+#body {
+ max-width: 800px;
+ margin: auto;
+}
+
+#header {
+ background-color: #3b5998;
+ padding: 5px;
+ padding-left: 10px;
+ padding-right: 10px;
+ margin-bottom: 1em;
+}
+
+#header,
+#header a {
+ color: white;
+}
+
+#header h1 a {
+ text-decoration: none;
+}
+
+#footer,
+#content {
+ margin-left: 10px;
+ margin-right: 10px;
+}
+
+#footer {
+ margin-top: 3em;
+}
+
+.entry h1 a {
+ color: black;
+ text-decoration: none;
+}
+
+.entry {
+ margin-bottom: 2em;
+}
+
+.entry .date {
+ margin-top: 3px;
+}
+
+.entry p {
+ margin: 0;
+ margin-bottom: 1em;
+}
+
+.entry .body {
+ margin-top: 1em;
+ line-height: 16pt;
+}
+
+.compose td {
+ vertical-align: middle;
+ padding-bottom: 5px;
+}
+
+.compose td.field {
+ padding-right: 10px;
+}
+
+.compose .title,
+.compose .submit {
+ font-family: "Helvetica Nue", Helvetica, Arial, sans-serif;
+ font-weight: bold;
+}
+
+.compose .title {
+ font-size: 20pt;
+}
+
+.compose .title,
+.compose .markdown {
+ width: 100%;
+}
+
+.compose .markdown {
+ height: 500px;
+ line-height: 16pt;
+}
diff --git a/vendor/tornado/demos/blog/templates/archive.html b/vendor/tornado/demos/blog/templates/archive.html
new file mode 100644
index 0000000000..dcca9511a4
--- /dev/null
+++ b/vendor/tornado/demos/blog/templates/archive.html
@@ -0,0 +1,31 @@
+{% extends "base.html" %}
+
+{% block head %}
+ <style type="text/css">
+ ul.archive {
+ list-style-type: none;
+ margin: 0;
+ padding: 0;
+ }
+
+ ul.archive li {
+ margin-bottom: 1em;
+ }
+
+ ul.archive .title {
+ font-family: "Helvetica Nue", Helvetica, Arial, sans-serif;
+ font-size: 14pt;
+ }
+ </style>
+{% end %}
+
+{% block body %}
+ <ul class="archive">
+ {% for entry in entries %}
+ <li>
+ <div class="title"><a href="/entry/{{ entry.slug }}">{{ escape(entry.title) }}</a></div>
+ <div class="date">{{ locale.format_date(entry.published, full_format=True, shorter=True) }}</div>
+ </li>
+ {% end %}
+ </ul>
+{% end %}
diff --git a/vendor/tornado/demos/blog/templates/base.html b/vendor/tornado/demos/blog/templates/base.html
new file mode 100644
index 0000000000..038c5b3fff
--- /dev/null
+++ b/vendor/tornado/demos/blog/templates/base.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>{{ escape(handler.settings["blog_title"]) }}</title>
+ <link rel="stylesheet" href="{{ static_url("blog.css") }}" type="text/css"/>
+ <link rel="alternate" href="/feed" type="application/atom+xml" title="{{ escape(handler.settings["blog_title"]) }}"/>
+ {% block head %}{% end %}
+ </head>
+ <body>
+ <div id="body">
+ <div id="header">
+ <div style="float:right">
+ {% if current_user %}
+ <a href="/compose">{{ _("New post") }}</a> -
+ <a href="/auth/logout?next={{ url_escape(request.uri) }}">{{ _("Sign out") }}</a>
+ {% else %}
+ {{ _('<a href="%(url)s">Sign in</a> to compose/edit') % {"url": "/auth/login?next=" + url_escape(request.uri)} }}
+ {% end %}
+ </div>
+ <h1><a href="/">{{ escape(handler.settings["blog_title"]) }}</a></h1>
+ </div>
+ <div id="content">{% block body %}{% end %}</div>
+ </div>
+ {% block bottom %}{% end %}
+ </body>
+</html>
diff --git a/vendor/tornado/demos/blog/templates/compose.html b/vendor/tornado/demos/blog/templates/compose.html
new file mode 100644
index 0000000000..bc054b3349
--- /dev/null
+++ b/vendor/tornado/demos/blog/templates/compose.html
@@ -0,0 +1,42 @@
+{% extends "base.html" %}
+
+{% block body %}
+ <form action="{{ request.path }}" method="post" class="compose">
+ <div style="margin-bottom:5px"><input name="title" type="text" class="title" value="{{ escape(entry.title) if entry else "" }}"/></div>
+ <div style="margin-bottom:5px"><textarea name="markdown" rows="30" cols="40" class="markdown">{{ escape(entry.markdown) if entry else "" }}</textarea></div>
+ <div>
+ <div style="float:right"><a href="http://daringfireball.net/projects/markdown/syntax">{{ _("Syntax documentation") }}</a></div>
+ <input type="submit" value="{{ _("Save changes") if entry else _("Publish post") }}" class="submit"/>
+ &nbsp;<a href="{{ "/entry/" + entry.slug if entry else "/" }}">{{ _("Cancel") }}</a>
+ </div>
+ {% if entry %}
+ <input type="hidden" name="id" value="{{ entry.id }}"/>
+ {% end %}
+ {{ xsrf_form_html() }}
+ </form>
+{% end %}
+
+{% block bottom %}
+ <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js" type="text/javascript"></script>
+ <script type="text/javascript">
+ //<![CDATA[
+
+ $(function() {
+ $("input[name=title]").select();
+ $("form.compose").submit(function() {
+ var required = ["title", "markdown"];
+ var form = $(this).get(0);
+ for (var i = 0; i < required.length; i++) {
+ if (!form[required[i]].value) {
+ $(form[required[i]]).select();
+ return false;
+ }
+ }
+ return true;
+ });
+ });
+
+ //]]>
+ </script>
+{% end %}
+
diff --git a/vendor/tornado/demos/blog/templates/entry.html b/vendor/tornado/demos/blog/templates/entry.html
new file mode 100644
index 0000000000..43c835dead
--- /dev/null
+++ b/vendor/tornado/demos/blog/templates/entry.html
@@ -0,0 +1,5 @@
+{% extends "base.html" %}
+
+{% block body %}
+ {{ modules.Entry(entry) }}
+{% end %}
diff --git a/vendor/tornado/demos/blog/templates/feed.xml b/vendor/tornado/demos/blog/templates/feed.xml
new file mode 100644
index 0000000000..c6c368656c
--- /dev/null
+++ b/vendor/tornado/demos/blog/templates/feed.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<feed xmlns="http://www.w3.org/2005/Atom">
+ {% set date_format = "%Y-%m-%dT%H:%M:%SZ" %}
+ <title>{{ escape(handler.settings["blog_title"]) }}</title>
+ {% if len(entries) > 0 %}
+ <updated>{{ max(e.updated for e in entries).strftime(date_format) }}</updated>
+ {% else %}
+ <updated>{{ datetime.datetime.utcnow().strftime(date_format) }}</updated>
+ {% end %}
+ <id>http://{{ request.host }}/</id>
+ <link rel="alternate" href="http://{{ request.host }}/" title="{{ escape(handler.settings["blog_title"]) }}" type="text/html"/>
+ <link rel="self" href="{{ request.full_url() }}" title="{{ escape(handler.settings["blog_title"]) }}" type="application/atom+xml"/>
+ <author><name>{{ escape(handler.settings["blog_title"]) }}</name></author>
+ {% for entry in entries %}
+ <entry>
+ <id>http://{{ request.host }}/entry/{{ entry.slug }}</id>
+ <title type="text">{{ escape(entry.title) }}</title>
+ <link href="http://{{ request.host }}/entry/{{ entry.slug }}" rel="alternate" type="text/html"/>
+ <updated>{{ entry.updated.strftime(date_format) }}</updated>
+ <published>{{ entry.published.strftime(date_format) }}</published>
+ <content type="xhtml" xml:base="http://{{ request.host }}/">
+ <div xmlns="http://www.w3.org/1999/xhtml">{{ entry.html }}</div>
+ </content>
+ </entry>
+ {% end %}
+</feed>
diff --git a/vendor/tornado/demos/blog/templates/home.html b/vendor/tornado/demos/blog/templates/home.html
new file mode 100644
index 0000000000..dd069a97f3
--- /dev/null
+++ b/vendor/tornado/demos/blog/templates/home.html
@@ -0,0 +1,8 @@
+{% extends "base.html" %}
+
+{% block body %}
+ {% for entry in entries %}
+ {{ modules.Entry(entry) }}
+ {% end %}
+ <div><a href="/archive">{{ _("Archive") }}</a></div>
+{% end %}
diff --git a/vendor/tornado/demos/blog/templates/modules/entry.html b/vendor/tornado/demos/blog/templates/modules/entry.html
new file mode 100644
index 0000000000..27ea0d76c2
--- /dev/null
+++ b/vendor/tornado/demos/blog/templates/modules/entry.html
@@ -0,0 +1,8 @@
+<div class="entry">
+ <h1><a href="/entry/{{ entry.slug }}">{{ escape(entry.title) }}</a></h1>
+ <div class="date">{{ locale.format_date(entry.published, full_format=True, shorter=True) }}</div>
+ <div class="body">{{ entry.html }}</div>
+ {% if current_user %}
+ <div class="admin"><a href="/compose?id={{ entry.id }}">{{ _("Edit this post") }}</a></div>
+ {% end %}
+</div>
diff --git a/vendor/tornado/demos/chat/chatdemo.py b/vendor/tornado/demos/chat/chatdemo.py
new file mode 100755
index 0000000000..7086592ec4
--- /dev/null
+++ b/vendor/tornado/demos/chat/chatdemo.py
@@ -0,0 +1,156 @@
+#!/usr/bin/env python
+#
+# Copyright 2009 Facebook
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import logging
+import tornado.auth
+import tornado.escape
+import tornado.httpserver
+import tornado.ioloop
+import tornado.options
+import tornado.web
+import os.path
+import uuid
+
+from tornado.options import define, options
+
+define("port", default=8888, help="run on the given port", type=int)
+
+
+class Application(tornado.web.Application):
+ def __init__(self):
+ handlers = [
+ (r"/", MainHandler),
+ (r"/auth/login", AuthLoginHandler),
+ (r"/auth/logout", AuthLogoutHandler),
+ (r"/a/message/new", MessageNewHandler),
+ (r"/a/message/updates", MessageUpdatesHandler),
+ ]
+ settings = dict(
+ cookie_secret="43oETzKXQAGaYdkL5gEmGeJJFuYh7EQnp2XdTP1o/Vo=",
+ login_url="/auth/login",
+ template_path=os.path.join(os.path.dirname(__file__), "templates"),
+ static_path=os.path.join(os.path.dirname(__file__), "static"),
+ xsrf_cookies=True,
+ )
+ tornado.web.Application.__init__(self, handlers, **settings)
+
+
+class BaseHandler(tornado.web.RequestHandler):
+ def get_current_user(self):
+ user_json = self.get_secure_cookie("user")
+ if not user_json: return None
+ return tornado.escape.json_decode(user_json)
+
+
+class MainHandler(BaseHandler):
+ @tornado.web.authenticated
+ def get(self):
+ self.render("index.html", messages=MessageMixin.cache)
+
+
+class MessageMixin(object):
+ waiters = []
+ cache = []
+ cache_size = 200
+
+ def wait_for_messages(self, callback, cursor=None):
+ cls = MessageMixin
+ if cursor:
+ index = 0
+ for i in xrange(len(cls.cache)):
+ index = len(cls.cache) - i - 1
+ if cls.cache[index]["id"] == cursor: break
+ recent = cls.cache[index + 1:]
+ if recent:
+ callback(recent)
+ return
+ cls.waiters.append(callback)
+
+ def new_messages(self, messages):
+ cls = MessageMixin
+ logging.info("Sending new message to %r listeners", len(cls.waiters))
+ for callback in cls.waiters:
+ try:
+ callback(messages)
+ except:
+ logging.error("Error in waiter callback", exc_info=True)
+ cls.waiters = []
+ cls.cache.extend(messages)
+ if len(cls.cache) > self.cache_size:
+ cls.cache = cls.cache[-self.cache_size:]
+
+
+class MessageNewHandler(BaseHandler, MessageMixin):
+ @tornado.web.authenticated
+ def post(self):
+ message = {
+ "id": str(uuid.uuid4()),
+ "from": self.current_user["first_name"],
+ "body": self.get_argument("body"),
+ }
+ message["html"] = self.render_string("message.html", message=message)
+ if self.get_argument("next", None):
+ self.redirect(self.get_argument("next"))
+ else:
+ self.write(message)
+ self.new_messages([message])
+
+
+class MessageUpdatesHandler(BaseHandler, MessageMixin):
+ @tornado.web.authenticated
+ @tornado.web.asynchronous
+ def post(self):
+ cursor = self.get_argument("cursor", None)
+ self.wait_for_messages(self.async_callback(self.on_new_messages),
+ cursor=cursor)
+
+ def on_new_messages(self, messages):
+ # Closed client connection
+ if self.request.connection.stream.closed():
+ return
+ self.finish(dict(messages=messages))
+
+
+class AuthLoginHandler(BaseHandler, tornado.auth.GoogleMixin):
+ @tornado.web.asynchronous
+ def get(self):
+ if self.get_argument("openid.mode", None):
+ self.get_authenticated_user(self.async_callback(self._on_auth))
+ return
+ self.authenticate_redirect(ax_attrs=["name"])
+
+ def _on_auth(self, user):
+ if not user:
+ raise tornado.web.HTTPError(500, "Google auth failed")
+ self.set_secure_cookie("user", tornado.escape.json_encode(user))
+ self.redirect("/")
+
+
+class AuthLogoutHandler(BaseHandler):
+ def get(self):
+ self.clear_cookie("user")
+ self.write("You are now logged out")
+
+
+def main():
+ tornado.options.parse_command_line()
+ http_server = tornado.httpserver.HTTPServer(Application())
+ http_server.listen(options.port)
+ tornado.ioloop.IOLoop.instance().start()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/vendor/tornado/demos/chat/static/chat.css b/vendor/tornado/demos/chat/static/chat.css
new file mode 100644
index 0000000000..a400c32605
--- /dev/null
+++ b/vendor/tornado/demos/chat/static/chat.css
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2009 FriendFeed
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License. You may obtain
+ * a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+body {
+ background: white;
+ margin: 10px;
+}
+
+body,
+input {
+ font-family: sans-serif;
+ font-size: 10pt;
+ color: black;
+}
+
+table {
+ border-collapse: collapse;
+ border: 0;
+}
+
+td {
+ border: 0;
+ padding: 0;
+}
+
+#body {
+ position: absolute;
+ bottom: 10px;
+ left: 10px;
+}
+
+#input {
+ margin-top: 0.5em;
+}
+
+#inbox .message {
+ padding-top: 0.25em;
+}
+
+#nav {
+ float: right;
+ z-index: 99;
+}
diff --git a/vendor/tornado/demos/chat/static/chat.js b/vendor/tornado/demos/chat/static/chat.js
new file mode 100644
index 0000000000..0054c710d6
--- /dev/null
+++ b/vendor/tornado/demos/chat/static/chat.js
@@ -0,0 +1,135 @@
+// Copyright 2009 FriendFeed
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+$(document).ready(function() {
+ if (!window.console) window.console = {};
+ if (!window.console.log) window.console.log = function() {};
+
+ $("#messageform").live("submit", function() {
+ newMessage($(this));
+ return false;
+ });
+ $("#messageform").live("keypress", function(e) {
+ if (e.keyCode == 13) {
+ newMessage($(this));
+ return false;
+ }
+ });
+ $("#message").select();
+ updater.poll();
+});
+
+function newMessage(form) {
+ var message = form.formToDict();
+ var disabled = form.find("input[type=submit]");
+ disabled.disable();
+ $.postJSON("/a/message/new", message, function(response) {
+ updater.showMessage(response);
+ if (message.id) {
+ form.parent().remove();
+ } else {
+ form.find("input[type=text]").val("").select();
+ disabled.enable();
+ }
+ });
+}
+
+function getCookie(name) {
+ var r = document.cookie.match("\\b" + name + "=([^;]*)\\b");
+ return r ? r[1] : undefined;
+}
+
+jQuery.postJSON = function(url, args, callback) {
+ args._xsrf = getCookie("_xsrf");
+ $.ajax({url: url, data: $.param(args), dataType: "text", type: "POST",
+ success: function(response) {
+ if (callback) callback(eval("(" + response + ")"));
+ }, error: function(response) {
+ console.log("ERROR:", response)
+ }});
+};
+
+jQuery.fn.formToDict = function() {
+ var fields = this.serializeArray();
+ var json = {}
+ for (var i = 0; i < fields.length; i++) {
+ json[fields[i].name] = fields[i].value;
+ }
+ if (json.next) delete json.next;
+ return json;
+};
+
+jQuery.fn.disable = function() {
+ this.enable(false);
+ return this;
+};
+
+jQuery.fn.enable = function(opt_enable) {
+ if (arguments.length && !opt_enable) {
+ this.attr("disabled", "disabled");
+ } else {
+ this.removeAttr("disabled");
+ }
+ return this;
+};
+
+var updater = {
+ errorSleepTime: 500,
+ cursor: null,
+
+ poll: function() {
+ var args = {"_xsrf": getCookie("_xsrf")};
+ if (updater.cursor) args.cursor = updater.cursor;
+ $.ajax({url: "/a/message/updates", type: "POST", dataType: "text",
+ data: $.param(args), success: updater.onSuccess,
+ error: updater.onError});
+ },
+
+ onSuccess: function(response) {
+ try {
+ updater.newMessages(eval("(" + response + ")"));
+ } catch (e) {
+ updater.onError();
+ return;
+ }
+ updater.errorSleepTime = 500;
+ window.setTimeout(updater.poll, 0);
+ },
+
+ onError: function(response) {
+ updater.errorSleepTime *= 2;
+ console.log("Poll error; sleeping for", updater.errorSleepTime, "ms");
+ window.setTimeout(updater.poll, updater.errorSleepTime);
+ },
+
+ newMessages: function(response) {
+ if (!response.messages) return;
+ updater.cursor = response.cursor;
+ var messages = response.messages;
+ updater.cursor = messages[messages.length - 1].id;
+ console.log(messages.length, "new messages, cursor:", updater.cursor);
+ for (var i = 0; i < messages.length; i++) {
+ updater.showMessage(messages[i]);
+ }
+ },
+
+ showMessage: function(message) {
+ var existing = $("#m" + message.id);
+ if (existing.length > 0) return;
+ var node = $(message.html);
+ node.hide();
+ $("#inbox").append(node);
+ node.slideDown();
+ }
+};
diff --git a/vendor/tornado/demos/chat/templates/index.html b/vendor/tornado/demos/chat/templates/index.html
new file mode 100644
index 0000000000..de051d852b
--- /dev/null
+++ b/vendor/tornado/demos/chat/templates/index.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>Tornado Chat Demo</title>
+ <link rel="stylesheet" href="{{ static_url("chat.css") }}" type="text/css"/>
+ </head>
+ <body>
+ <div id="nav">
+ <b>{{ escape(current_user["name"]) }}</b> -
+ <a href="/auth/logout">{{ _("Sign out") }}</a>
+ </div>
+ <div id="body">
+ <div id="inbox">
+ {% for message in messages %}
+ {% include "message.html" %}
+ {% end %}
+ </div>
+ <div id="input">
+ <form action="/a/message/new" method="post" id="messageform">
+ <table>
+ <tr>
+ <td><input name="body" id="message" style="width:500px"/></td>
+ <td style="padding-left:5px">
+ <input type="submit" value="{{ _("Post") }}"/>
+ <input type="hidden" name="next" value="{{ request.path }}"/>
+ {{ xsrf_form_html() }}
+ </td>
+ </tr>
+ </table>
+ </form>
+ </div>
+ </div>
+ <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.3/jquery.min.js" type="text/javascript"></script>
+ <script src="{{ static_url("chat.js") }}" type="text/javascript"></script>
+ </body>
+</html>
diff --git a/vendor/tornado/demos/chat/templates/message.html b/vendor/tornado/demos/chat/templates/message.html
new file mode 100644
index 0000000000..4445cbdfaf
--- /dev/null
+++ b/vendor/tornado/demos/chat/templates/message.html
@@ -0,0 +1 @@
+<div class="message" id="m{{ message["id"] }}"><b>{{ escape(message["from"]) }}: </b>{{ escape(message["body"]) }}</div>
diff --git a/vendor/tornado/demos/facebook/README b/vendor/tornado/demos/facebook/README
new file mode 100644
index 0000000000..2f0dc28e84
--- /dev/null
+++ b/vendor/tornado/demos/facebook/README
@@ -0,0 +1,8 @@
+Running the Tornado Facebook example
+=====================================
+To work with the provided Facebook api key, this example must be
+accessed at http://localhost:8888/ to match the Connect URL set in the
+example application.
+
+To use any other domain, a new Facebook application must be registered
+with a Connect URL set to that domain.
diff --git a/vendor/tornado/demos/facebook/facebook.py b/vendor/tornado/demos/facebook/facebook.py
new file mode 100755
index 0000000000..0c984ddaa0
--- /dev/null
+++ b/vendor/tornado/demos/facebook/facebook.py
@@ -0,0 +1,127 @@
+#!/usr/bin/env python
+#
+# Copyright 2009 Facebook
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import logging
+import os.path
+import tornado.auth
+import tornado.escape
+import tornado.httpserver
+import tornado.ioloop
+import tornado.options
+import tornado.web
+import uimodules
+
+from tornado.options import define, options
+
+define("port", default=8888, help="run on the given port", type=int)
+define("facebook_api_key", help="your Facebook application API key",
+ default="9e2ada1b462142c4dfcc8e894ea1e37c")
+define("facebook_secret", help="your Facebook application secret",
+ default="32fc6114554e3c53d5952594510021e2")
+
+
+class Application(tornado.web.Application):
+ def __init__(self):
+ handlers = [
+ (r"/", MainHandler),
+ (r"/auth/login", AuthLoginHandler),
+ (r"/auth/logout", AuthLogoutHandler),
+ ]
+ settings = dict(
+ cookie_secret="12oETzKXQAGaYdkL5gEmGeJJFuYh7EQnp2XdTP1o/Vo=",
+ login_url="/auth/login",
+ template_path=os.path.join(os.path.dirname(__file__), "templates"),
+ static_path=os.path.join(os.path.dirname(__file__), "static"),
+ xsrf_cookies=True,
+ facebook_api_key=options.facebook_api_key,
+ facebook_secret=options.facebook_secret,
+ ui_modules= {"Post": PostModule},
+ debug=True,
+ )
+ tornado.web.Application.__init__(self, handlers, **settings)
+
+
+class BaseHandler(tornado.web.RequestHandler):
+ def get_current_user(self):
+ user_json = self.get_secure_cookie("user")
+ if not user_json: return None
+ return tornado.escape.json_decode(user_json)
+
+
+class MainHandler(BaseHandler, tornado.auth.FacebookMixin):
+ @tornado.web.authenticated
+ @tornado.web.asynchronous
+ def get(self):
+ self.facebook_request(
+ method="stream.get",
+ callback=self.async_callback(self._on_stream),
+ session_key=self.current_user["session_key"])
+
+ def _on_stream(self, stream):
+ if stream is None:
+ # Session may have expired
+ self.redirect("/auth/login")
+ return
+ # Turn profiles into a dict mapping id => profile
+ stream["profiles"] = dict((p["id"], p) for p in stream["profiles"])
+ self.render("stream.html", stream=stream)
+
+
+class AuthLoginHandler(BaseHandler, tornado.auth.FacebookMixin):
+ @tornado.web.asynchronous
+ def get(self):
+ if self.get_argument("session", None):
+ self.get_authenticated_user(self.async_callback(self._on_auth))
+ return
+ self.authorize_redirect("read_stream")
+
+ def _on_auth(self, user):
+ if not user:
+ raise tornado.web.HTTPError(500, "Facebook auth failed")
+ self.set_secure_cookie("user", tornado.escape.json_encode(user))
+ self.redirect(self.get_argument("next", "/"))
+
+
+class AuthLogoutHandler(BaseHandler, tornado.auth.FacebookMixin):
+ @tornado.web.asynchronous
+ def get(self):
+ self.clear_cookie("user")
+ if not self.current_user:
+ self.redirect(self.get_argument("next", "/"))
+ return
+ self.facebook_request(
+ method="auth.revokeAuthorization",
+ callback=self.async_callback(self._on_deauthorize),
+ session_key=self.current_user["session_key"])
+
+ def _on_deauthorize(self, response):
+ self.redirect(self.get_argument("next", "/"))
+
+
+class PostModule(tornado.web.UIModule):
+ def render(self, post, actor):
+ return self.render_string("modules/post.html", post=post, actor=actor)
+
+
+def main():
+ tornado.options.parse_command_line()
+ http_server = tornado.httpserver.HTTPServer(Application())
+ http_server.listen(options.port)
+ tornado.ioloop.IOLoop.instance().start()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/vendor/tornado/demos/facebook/static/facebook.css b/vendor/tornado/demos/facebook/static/facebook.css
new file mode 100644
index 0000000000..4fee72678f
--- /dev/null
+++ b/vendor/tornado/demos/facebook/static/facebook.css
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2009 Facebook
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License. You may obtain
+ * a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+body {
+ background: white;
+ color: black;
+ margin: 15px;
+}
+
+body,
+input,
+textarea {
+ font-family: "Lucida Grande", Tahoma, Verdana, sans-serif;
+ font-size: 10pt;
+}
+
+table {
+ border-collapse: collapse;
+ border: 0;
+}
+
+td {
+ border: 0;
+ padding: 0;
+}
+
+img {
+ border: 0;
+}
+
+a {
+ text-decoration: none;
+ color: #3b5998;
+}
+
+a:hover {
+ text-decoration: underline;
+}
+
+.post {
+ border-bottom: 1px solid #eeeeee;
+ min-height: 50px;
+ padding-bottom: 10px;
+ margin-top: 10px;
+}
+
+.post .picture {
+ float: left;
+}
+
+.post .picture img {
+ height: 50px;
+ width: 50px;
+}
+
+.post .body {
+ margin-left: 60px;
+}
+
+.post .media img {
+ border: 1px solid #cccccc;
+ padding: 3px;
+}
+
+.post .media:hover img {
+ border: 1px solid #3b5998;
+}
+
+.post a.actor {
+ font-weight: bold;
+}
+
+.post .meta {
+ font-size: 11px;
+}
+
+.post a.permalink {
+ color: #777777;
+}
+
+#body {
+ max-width: 700px;
+ margin: auto;
+}
diff --git a/vendor/tornado/demos/facebook/static/facebook.js b/vendor/tornado/demos/facebook/static/facebook.js
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/vendor/tornado/demos/facebook/static/facebook.js
diff --git a/vendor/tornado/demos/facebook/templates/modules/post.html b/vendor/tornado/demos/facebook/templates/modules/post.html
new file mode 100644
index 0000000000..6b50ac0f72
--- /dev/null
+++ b/vendor/tornado/demos/facebook/templates/modules/post.html
@@ -0,0 +1,29 @@
+<div class="post">
+ <div class="picture">
+ <a href="{{ actor["url"] }}"><img src="{{ actor["pic_square"] }}"/></a>
+ </div>
+ <div class="body">
+ <a href="{{ actor["url"] }}" class="actor">{{ escape(actor["name"]) }}</a>
+ {% if post["message"] %}
+ <span class="message">{{ escape(post["message"]) }}</span>
+ {% end %}
+ {% if post["attachment"] %}
+ <div class="attachment">
+ {% if post["attachment"].get("name") %}
+ <div class="name"><a href="{{ post["attachment"]["href"] }}">{{ escape(post["attachment"]["name"]) }}</a></div>
+ {% end %}
+ {% if post["attachment"].get("description") %}
+ <div class="description">{{ post["attachment"]["description"] }}</div>
+ {% end %}
+ {% for media in filter(lambda m: m.get("src") and m["type"] in ("photo", "link"), post["attachment"].get("media", [])) %}
+ <span class="media">
+ <a href="{{ media["href"] }}"><img src="{{ media["src"] }}" alt="{{ escape(media.get("alt", "")) }}"/></a>
+ </span>
+ {% end %}
+ </div>
+ {% end %}
+ <div class="meta">
+ <a href="{{ post["permalink"] }}" class="permalink">{{ locale.format_date(post["created_time"]) }}</a>
+ </div>
+ </div>
+</div>
diff --git a/vendor/tornado/demos/facebook/templates/stream.html b/vendor/tornado/demos/facebook/templates/stream.html
new file mode 100644
index 0000000000..19baa28cbf
--- /dev/null
+++ b/vendor/tornado/demos/facebook/templates/stream.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>Tornado Facebook Stream Demo</title>
+ <link rel="stylesheet" href="{{ static_url("facebook.css") }}" type="text/css"/>
+ </head>
+ <body>
+ <div id="body">
+ <div style="float:right">
+ <b>{{ escape(current_user["name"]) }}</b> -
+ <a href="/auth/logout">{{ _("Sign out") }}</a>
+ </div>
+ <div style="margin-bottom:1em"><a href="/">{{ _("Refresh stream") }}</a></div>
+ <div id="stream">
+ {% for post in stream["posts"] %}
+ {{ modules.Post(post, stream["profiles"][post["actor_id"]]) }}
+ {% end %}
+ </div>
+ </div>
+ </body>
+</html>
diff --git a/vendor/tornado/demos/facebook/uimodules.py b/vendor/tornado/demos/facebook/uimodules.py
new file mode 100644
index 0000000000..1173db634e
--- /dev/null
+++ b/vendor/tornado/demos/facebook/uimodules.py
@@ -0,0 +1,22 @@
+#!/usr/bin/env python
+#
+# Copyright 2009 Facebook
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import tornado.web
+
+
+class Entry(tornado.web.UIModule):
+ def render(self):
+ return '<div>ENTRY</div>'
diff --git a/vendor/tornado/demos/helloworld/helloworld.py b/vendor/tornado/demos/helloworld/helloworld.py
new file mode 100755
index 0000000000..0f1ed61ff5
--- /dev/null
+++ b/vendor/tornado/demos/helloworld/helloworld.py
@@ -0,0 +1,43 @@
+#!/usr/bin/env python
+#
+# Copyright 2009 Facebook
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import tornado.httpserver
+import tornado.ioloop
+import tornado.options
+import tornado.web
+
+from tornado.options import define, options
+
+define("port", default=8888, help="run on the given port", type=int)
+
+
+class MainHandler(tornado.web.RequestHandler):
+ def get(self):
+ self.write("Hello, world")
+
+
+def main():
+ tornado.options.parse_command_line()
+ application = tornado.web.Application([
+ (r"/", MainHandler),
+ ])
+ http_server = tornado.httpserver.HTTPServer(application)
+ http_server.listen(options.port)
+ tornado.ioloop.IOLoop.instance().start()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/vendor/tornado/setup.py b/vendor/tornado/setup.py
new file mode 100644
index 0000000000..5cb69df2da
--- /dev/null
+++ b/vendor/tornado/setup.py
@@ -0,0 +1,44 @@
+#!/usr/bin/env python
+#
+# Copyright 2009 Facebook
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import distutils.core
+import sys
+# Importing setuptools adds some features like "setup.py develop", but
+# it's optional so swallow the error if it's not there.
+try:
+ import setuptools
+except ImportError:
+ pass
+
+# Build the epoll extension for Linux systems with Python < 2.6
+extensions = []
+major, minor = sys.version_info[:2]
+python_26 = (major > 2 or (major == 2 and minor >= 6))
+if "linux" in sys.platform.lower() and not python_26:
+ extensions.append(distutils.core.Extension(
+ "tornado.epoll", ["tornado/epoll.c"]))
+
+distutils.core.setup(
+ name="tornado",
+ version="0.2",
+ packages = ["tornado"],
+ ext_modules = extensions,
+ author="Facebook",
+ author_email="python-tornado@googlegroups.com",
+ url="http://www.tornadoweb.org/",
+ license="http://www.apache.org/licenses/LICENSE-2.0",
+ description="Tornado is an open source version of the scalable, non-blocking web server and and tools that power FriendFeed",
+)
diff --git a/vendor/tornado/tornado/__init__.py b/vendor/tornado/tornado/__init__.py
new file mode 100644
index 0000000000..8f73764eb2
--- /dev/null
+++ b/vendor/tornado/tornado/__init__.py
@@ -0,0 +1,17 @@
+#!/usr/bin/env python
+#
+# Copyright 2009 Facebook
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""The Tornado web server and tools."""
diff --git a/vendor/tornado/tornado/auth.py b/vendor/tornado/tornado/auth.py
new file mode 100644
index 0000000000..f67d9e5482
--- /dev/null
+++ b/vendor/tornado/tornado/auth.py
@@ -0,0 +1,883 @@
+#!/usr/bin/env python
+#
+# Copyright 2009 Facebook
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""Implementations of various third-party authentication schemes.
+
+All the classes in this file are class Mixins designed to be used with
+web.py RequestHandler classes. The primary methods for each service are
+authenticate_redirect(), authorize_redirect(), and get_authenticated_user().
+The former should be called to redirect the user to, e.g., the OpenID
+authentication page on the third party service, and the latter should
+be called upon return to get the user data from the data returned by
+the third party service.
+
+They all take slightly different arguments due to the fact all these
+services implement authentication and authorization slightly differently.
+See the individual service classes below for complete documentation.
+
+Example usage for Google OpenID:
+
+class GoogleHandler(tornado.web.RequestHandler, tornado.auth.GoogleMixin):
+ @tornado.web.asynchronous
+ def get(self):
+ if self.get_argument("openid.mode", None):
+ self.get_authenticated_user(self.async_callback(self._on_auth))
+ return
+ self.authenticate_redirect()
+
+ def _on_auth(self, user):
+ if not user:
+ raise tornado.web.HTTPError(500, "Google auth failed")
+ # Save the user with, e.g., set_secure_cookie()
+
+"""
+
+import base64
+import binascii
+import cgi
+import hashlib
+import hmac
+import httpclient
+import escape
+import logging
+import time
+import urllib
+import urlparse
+import uuid
+
+_log = logging.getLogger("tornado.auth")
+
+class OpenIdMixin(object):
+ """Abstract implementation of OpenID and Attribute Exchange.
+
+ See GoogleMixin below for example implementations.
+ """
+ def authenticate_redirect(self, callback_uri=None,
+ ax_attrs=["name","email","language","username"]):
+ """Returns the authentication URL for this service.
+
+ After authentication, the service will redirect back to the given
+ callback URI.
+
+ We request the given attributes for the authenticated user by
+ default (name, email, language, and username). If you don't need
+ all those attributes for your app, you can request fewer with
+ the ax_attrs keyword argument.
+ """
+ callback_uri = callback_uri or self.request.path
+ args = self._openid_args(callback_uri, ax_attrs=ax_attrs)
+ self.redirect(self._OPENID_ENDPOINT + "?" + urllib.urlencode(args))
+
+ def get_authenticated_user(self, callback):
+ """Fetches the authenticated user data upon redirect.
+
+ This method should be called by the handler that receives the
+ redirect from the authenticate_redirect() or authorize_redirect()
+ methods.
+ """
+ # Verify the OpenID response via direct request to the OP
+ args = dict((k, v[-1]) for k, v in self.request.arguments.iteritems())
+ args["openid.mode"] = u"check_authentication"
+ url = self._OPENID_ENDPOINT + "?" + urllib.urlencode(args)
+ http = httpclient.AsyncHTTPClient()
+ http.fetch(url, self.async_callback(
+ self._on_authentication_verified, callback))
+
+ def _openid_args(self, callback_uri, ax_attrs=[], oauth_scope=None):
+ url = urlparse.urljoin(self.request.full_url(), callback_uri)
+ args = {
+ "openid.ns": "http://specs.openid.net/auth/2.0",
+ "openid.claimed_id":
+ "http://specs.openid.net/auth/2.0/identifier_select",
+ "openid.identity":
+ "http://specs.openid.net/auth/2.0/identifier_select",
+ "openid.return_to": url,
+ "openid.realm": "http://" + self.request.host + "/",
+ "openid.mode": "checkid_setup",
+ }
+ if ax_attrs:
+ args.update({
+ "openid.ns.ax": "http://openid.net/srv/ax/1.0",
+ "openid.ax.mode": "fetch_request",
+ })
+ ax_attrs = set(ax_attrs)
+ required = []
+ if "name" in ax_attrs:
+ ax_attrs -= set(["name", "firstname", "fullname", "lastname"])
+ required += ["firstname", "fullname", "lastname"]
+ args.update({
+ "openid.ax.type.firstname":
+ "http://axschema.org/namePerson/first",
+ "openid.ax.type.fullname":
+ "http://axschema.org/namePerson",
+ "openid.ax.type.lastname":
+ "http://axschema.org/namePerson/last",
+ })
+ known_attrs = {
+ "email": "http://axschema.org/contact/email",
+ "language": "http://axschema.org/pref/language",
+ "username": "http://axschema.org/namePerson/friendly",
+ }
+ for name in ax_attrs:
+ args["openid.ax.type." + name] = known_attrs[name]
+ required.append(name)
+ args["openid.ax.required"] = ",".join(required)
+ if oauth_scope:
+ args.update({
+ "openid.ns.oauth":
+ "http://specs.openid.net/extensions/oauth/1.0",
+ "openid.oauth.consumer": self.request.host.split(":")[0],
+ "openid.oauth.scope": oauth_scope,
+ })
+ return args
+
+ def _on_authentication_verified(self, callback, response):
+ if response.error or u"is_valid:true" not in response.body:
+ _log.warning("Invalid OpenID response: %s", response.error or
+ response.body)
+ callback(None)
+ return
+
+ # Make sure we got back at least an email from attribute exchange
+ ax_ns = None
+ for name, values in self.request.arguments.iteritems():
+ if name.startswith("openid.ns.") and \
+ values[-1] == u"http://openid.net/srv/ax/1.0":
+ ax_ns = name[10:]
+ break
+ def get_ax_arg(uri):
+ if not ax_ns: return u""
+ prefix = "openid." + ax_ns + ".type."
+ ax_name = None
+ for name, values in self.request.arguments.iteritems():
+ if values[-1] == uri and name.startswith(prefix):
+ part = name[len(prefix):]
+ ax_name = "openid." + ax_ns + ".value." + part
+ break
+ if not ax_name: return u""
+ return self.get_argument(ax_name, u"")
+
+ email = get_ax_arg("http://axschema.org/contact/email")
+ name = get_ax_arg("http://axschema.org/namePerson")
+ first_name = get_ax_arg("http://axschema.org/namePerson/first")
+ last_name = get_ax_arg("http://axschema.org/namePerson/last")
+ username = get_ax_arg("http://axschema.org/namePerson/friendly")
+ locale = get_ax_arg("http://axschema.org/pref/language").lower()
+ user = dict()
+ name_parts = []
+ if first_name:
+ user["first_name"] = first_name
+ name_parts.append(first_name)
+ if last_name:
+ user["last_name"] = last_name
+ name_parts.append(last_name)
+ if name:
+ user["name"] = name
+ elif name_parts:
+ user["name"] = u" ".join(name_parts)
+ elif email:
+ user["name"] = email.split("@")[0]
+ if email: user["email"] = email
+ if locale: user["locale"] = locale
+ if username: user["username"] = username
+ callback(user)
+
+
+class OAuthMixin(object):
+ """Abstract implementation of OAuth.
+
+ See TwitterMixin and FriendFeedMixin below for example implementations.
+ """
+ def authorize_redirect(self, callback_uri=None):
+ """Redirects the user to obtain OAuth authorization for this service.
+
+ Twitter and FriendFeed both require that you register a Callback
+ URL with your application. You should call this method to log the
+ user in, and then call get_authenticated_user() in the handler
+ you registered as your Callback URL to complete the authorization
+ process.
+
+ This method sets a cookie called _oauth_request_token which is
+ subsequently used (and cleared) in get_authenticated_user for
+ security purposes.
+ """
+ if callback_uri and getattr(self, "_OAUTH_NO_CALLBACKS", False):
+ raise Exception("This service does not support oauth_callback")
+ http = httpclient.AsyncHTTPClient()
+ http.fetch(self._oauth_request_token_url(), self.async_callback(
+ self._on_request_token, self._OAUTH_AUTHORIZE_URL, callback_uri))
+
+ def get_authenticated_user(self, callback):
+ """Gets the OAuth authorized user and access token on callback.
+
+ This method should be called from the handler for your registered
+ OAuth Callback URL to complete the registration process. We call
+ callback with the authenticated user, which in addition to standard
+ attributes like 'name' includes the 'access_key' attribute, which
+ contains the OAuth access you can use to make authorized requests
+ to this service on behalf of the user.
+ """
+ request_key = self.get_argument("oauth_token")
+ request_cookie = self.get_cookie("_oauth_request_token")
+ if not request_cookie:
+ _log.warning("Missing OAuth request token cookie")
+ callback(None)
+ return
+ cookie_key, cookie_secret = request_cookie.split("|")
+ if cookie_key != request_key:
+ _log.warning("Request token does not match cookie")
+ callback(None)
+ return
+ token = dict(key=cookie_key, secret=cookie_secret)
+ http = httpclient.AsyncHTTPClient()
+ http.fetch(self._oauth_access_token_url(token), self.async_callback(
+ self._on_access_token, callback))
+
+ def _oauth_request_token_url(self):
+ consumer_token = self._oauth_consumer_token()
+ url = self._OAUTH_REQUEST_TOKEN_URL
+ args = dict(
+ oauth_consumer_key=consumer_token["key"],
+ oauth_signature_method="HMAC-SHA1",
+ oauth_timestamp=str(int(time.time())),
+ oauth_nonce=binascii.b2a_hex(uuid.uuid4().bytes),
+ oauth_version="1.0",
+ )
+ signature = _oauth_signature(consumer_token, "GET", url, args)
+ args["oauth_signature"] = signature
+ return url + "?" + urllib.urlencode(args)
+
+ def _on_request_token(self, authorize_url, callback_uri, response):
+ if response.error:
+ raise Exception("Could not get request token")
+ request_token = _oauth_parse_response(response.body)
+ data = "|".join([request_token["key"], request_token["secret"]])
+ self.set_cookie("_oauth_request_token", data)
+ args = dict(oauth_token=request_token["key"])
+ if callback_uri:
+ args["oauth_callback"] = urlparse.urljoin(
+ self.request.full_url(), callback_uri)
+ self.redirect(authorize_url + "?" + urllib.urlencode(args))
+
+ def _oauth_access_token_url(self, request_token):
+ consumer_token = self._oauth_consumer_token()
+ url = self._OAUTH_ACCESS_TOKEN_URL
+ args = dict(
+ oauth_consumer_key=consumer_token["key"],
+ oauth_token=request_token["key"],
+ oauth_signature_method="HMAC-SHA1",
+ oauth_timestamp=str(int(time.time())),
+ oauth_nonce=binascii.b2a_hex(uuid.uuid4().bytes),
+ oauth_version="1.0",
+ )
+ signature = _oauth_signature(consumer_token, "GET", url, args,
+ request_token)
+ args["oauth_signature"] = signature
+ return url + "?" + urllib.urlencode(args)
+
+ def _on_access_token(self, callback, response):
+ if response.error:
+ _log.warning("Could not fetch access token")
+ callback(None)
+ return
+ access_token = _oauth_parse_response(response.body)
+ user = self._oauth_get_user(access_token, self.async_callback(
+ self._on_oauth_get_user, access_token, callback))
+
+ def _oauth_get_user(self, access_token, callback):
+ raise NotImplementedError()
+
+ def _on_oauth_get_user(self, access_token, callback, user):
+ if not user:
+ callback(None)
+ return
+ user["access_token"] = access_token
+ callback(user)
+
+ def _oauth_request_parameters(self, url, access_token, parameters={},
+ method="GET"):
+ """Returns the OAuth parameters as a dict for the given request.
+
+ parameters should include all POST arguments and query string arguments
+ that will be sent with the request.
+ """
+ consumer_token = self._oauth_consumer_token()
+ base_args = dict(
+ oauth_consumer_key=consumer_token["key"],
+ oauth_token=access_token["key"],
+ oauth_signature_method="HMAC-SHA1",
+ oauth_timestamp=str(int(time.time())),
+ oauth_nonce=binascii.b2a_hex(uuid.uuid4().bytes),
+ oauth_version="1.0",
+ )
+ args = {}
+ args.update(base_args)
+ args.update(parameters)
+ signature = _oauth_signature(consumer_token, method, url, args,
+ access_token)
+ base_args["oauth_signature"] = signature
+ return base_args
+
+
+class TwitterMixin(OAuthMixin):
+ """Twitter OAuth authentication.
+
+ To authenticate with Twitter, register your application with
+ Twitter at http://twitter.com/apps. Then copy your Consumer Key and
+ Consumer Secret to the application settings 'twitter_consumer_key' and
+ 'twitter_consumer_secret'. Use this Mixin on the handler for the URL
+ you registered as your application's Callback URL.
+
+ When your application is set up, you can use this Mixin like this
+ to authenticate the user with Twitter and get access to their stream:
+
+ class TwitterHandler(tornado.web.RequestHandler,
+ tornado.auth.TwitterMixin):
+ @tornado.web.asynchronous
+ def get(self):
+ if self.get_argument("oauth_token", None):
+ self.get_authenticated_user(self.async_callback(self._on_auth))
+ return
+ self.authorize_redirect()
+
+ def _on_auth(self, user):
+ if not user:
+ raise tornado.web.HTTPError(500, "Twitter auth failed")
+ # Save the user using, e.g., set_secure_cookie()
+
+ The user object returned by get_authenticated_user() includes the
+ attributes 'username', 'name', and all of the custom Twitter user
+ attributes describe at
+ http://apiwiki.twitter.com/Twitter-REST-API-Method%3A-users%C2%A0show
+ in addition to 'access_token'. You should save the access token with
+ the user; it is required to make requests on behalf of the user later
+ with twitter_request().
+ """
+ _OAUTH_REQUEST_TOKEN_URL = "http://twitter.com/oauth/request_token"
+ _OAUTH_ACCESS_TOKEN_URL = "http://twitter.com/oauth/access_token"
+ _OAUTH_AUTHORIZE_URL = "http://twitter.com/oauth/authorize"
+ _OAUTH_AUTHENTICATE_URL = "http://twitter.com/oauth/authenticate"
+ _OAUTH_NO_CALLBACKS = True
+
+ def authenticate_redirect(self):
+ """Just like authorize_redirect(), but auto-redirects if authorized.
+
+ This is generally the right interface to use if you are using
+ Twitter for single-sign on.
+ """
+ http = httpclient.AsyncHTTPClient()
+ http.fetch(self._oauth_request_token_url(), self.async_callback(
+ self._on_request_token, self._OAUTH_AUTHENTICATE_URL, None))
+
+ def twitter_request(self, path, callback, access_token=None,
+ post_args=None, **args):
+ """Fetches the given API path, e.g., "/statuses/user_timeline/btaylor"
+
+ The path should not include the format (we automatically append
+ ".json" and parse the JSON output).
+
+ If the request is a POST, post_args should be provided. Query
+ string arguments should be given as keyword arguments.
+
+ All the Twitter methods are documented at
+ http://apiwiki.twitter.com/Twitter-API-Documentation.
+
+ Many methods require an OAuth access token which you can obtain
+ through authorize_redirect() and get_authenticated_user(). The
+ user returned through that process includes an 'access_token'
+ attribute that can be used to make authenticated requests via
+ this method. Example usage:
+
+ class MainHandler(tornado.web.RequestHandler,
+ tornado.auth.TwitterMixin):
+ @tornado.web.authenticated
+ @tornado.web.asynchronous
+ def get(self):
+ self.twitter_request(
+ "/statuses/update",
+ post_args={"status": "Testing Tornado Web Server"},
+ access_token=user["access_token"],
+ callback=self.async_callback(self._on_post))
+
+ def _on_post(self, new_entry):
+ if not new_entry:
+ # Call failed; perhaps missing permission?
+ self.authorize_redirect()
+ return
+ self.finish("Posted a message!")
+
+ """
+ # Add the OAuth resource request signature if we have credentials
+ url = "http://twitter.com" + path + ".json"
+ if access_token:
+ all_args = {}
+ all_args.update(args)
+ all_args.update(post_args or {})
+ consumer_token = self._oauth_consumer_token()
+ method = "POST" if post_args is not None else "GET"
+ oauth = self._oauth_request_parameters(
+ url, access_token, all_args, method=method)
+ args.update(oauth)
+ if args: url += "?" + urllib.urlencode(args)
+ callback = self.async_callback(self._on_twitter_request, callback)
+ http = httpclient.AsyncHTTPClient()
+ if post_args is not None:
+ http.fetch(url, method="POST", body=urllib.urlencode(post_args),
+ callback=callback)
+ else:
+ http.fetch(url, callback=callback)
+
+ def _on_twitter_request(self, callback, response):
+ if response.error:
+ _log.warning("Error response %s fetching %s", response.error,
+ response.request.url)
+ callback(None)
+ return
+ callback(escape.json_decode(response.body))
+
+ def _oauth_consumer_token(self):
+ self.require_setting("twitter_consumer_key", "Twitter OAuth")
+ self.require_setting("twitter_consumer_secret", "Twitter OAuth")
+ return dict(
+ key=self.settings["twitter_consumer_key"],
+ secret=self.settings["twitter_consumer_secret"])
+
+ def _oauth_get_user(self, access_token, callback):
+ callback = self.async_callback(self._parse_user_response, callback)
+ self.twitter_request(
+ "/users/show/" + access_token["screen_name"],
+ access_token=access_token, callback=callback)
+
+ def _parse_user_response(self, callback, user):
+ if user:
+ user["username"] = user["screen_name"]
+ callback(user)
+
+
+class FriendFeedMixin(OAuthMixin):
+ """FriendFeed OAuth authentication.
+
+ To authenticate with FriendFeed, register your application with
+ FriendFeed at http://friendfeed.com/api/applications. Then
+ copy your Consumer Key and Consumer Secret to the application settings
+ 'friendfeed_consumer_key' and 'friendfeed_consumer_secret'. Use
+ this Mixin on the handler for the URL you registered as your
+ application's Callback URL.
+
+ When your application is set up, you can use this Mixin like this
+ to authenticate the user with FriendFeed and get access to their feed:
+
+ class FriendFeedHandler(tornado.web.RequestHandler,
+ tornado.auth.FriendFeedMixin):
+ @tornado.web.asynchronous
+ def get(self):
+ if self.get_argument("oauth_token", None):
+ self.get_authenticated_user(self.async_callback(self._on_auth))
+ return
+ self.authorize_redirect()
+
+ def _on_auth(self, user):
+ if not user:
+ raise tornado.web.HTTPError(500, "FriendFeed auth failed")
+ # Save the user using, e.g., set_secure_cookie()
+
+ The user object returned by get_authenticated_user() includes the
+ attributes 'username', 'name', and 'description' in addition to
+ 'access_token'. You should save the access token with the user;
+ it is required to make requests on behalf of the user later with
+ friendfeed_request().
+ """
+ _OAUTH_REQUEST_TOKEN_URL = "https://friendfeed.com/account/oauth/request_token"
+ _OAUTH_ACCESS_TOKEN_URL = "https://friendfeed.com/account/oauth/access_token"
+ _OAUTH_AUTHORIZE_URL = "https://friendfeed.com/account/oauth/authorize"
+ _OAUTH_NO_CALLBACKS = True
+
+ def friendfeed_request(self, path, callback, access_token=None,
+ post_args=None, **args):
+ """Fetches the given relative API path, e.g., "/bret/friends"
+
+ If the request is a POST, post_args should be provided. Query
+ string arguments should be given as keyword arguments.
+
+ All the FriendFeed methods are documented at
+ http://friendfeed.com/api/documentation.
+
+ Many methods require an OAuth access token which you can obtain
+ through authorize_redirect() and get_authenticated_user(). The
+ user returned through that process includes an 'access_token'
+ attribute that can be used to make authenticated requests via
+ this method. Example usage:
+
+ class MainHandler(tornado.web.RequestHandler,
+ tornado.auth.FriendFeedMixin):
+ @tornado.web.authenticated
+ @tornado.web.asynchronous
+ def get(self):
+ self.friendfeed_request(
+ "/entry",
+ post_args={"body": "Testing Tornado Web Server"},
+ access_token=self.current_user["access_token"],
+ callback=self.async_callback(self._on_post))
+
+ def _on_post(self, new_entry):
+ if not new_entry:
+ # Call failed; perhaps missing permission?
+ self.authorize_redirect()
+ return
+ self.finish("Posted a message!")
+
+ """
+ # Add the OAuth resource request signature if we have credentials
+ url = "http://friendfeed-api.com/v2" + path
+ if access_token:
+ all_args = {}
+ all_args.update(args)
+ all_args.update(post_args or {})
+ consumer_token = self._oauth_consumer_token()
+ method = "POST" if post_args is not None else "GET"
+ oauth = self._oauth_request_parameters(
+ url, access_token, all_args, method=method)
+ args.update(oauth)
+ if args: url += "?" + urllib.urlencode(args)
+ callback = self.async_callback(self._on_friendfeed_request, callback)
+ http = httpclient.AsyncHTTPClient()
+ if post_args is not None:
+ http.fetch(url, method="POST", body=urllib.urlencode(post_args),
+ callback=callback)
+ else:
+ http.fetch(url, callback=callback)
+
+ def _on_friendfeed_request(self, callback, response):
+ if response.error:
+ _log.warning("Error response %s fetching %s", response.error,
+ response.request.url)
+ callback(None)
+ return
+ callback(escape.json_decode(response.body))
+
+ def _oauth_consumer_token(self):
+ self.require_setting("friendfeed_consumer_key", "FriendFeed OAuth")
+ self.require_setting("friendfeed_consumer_secret", "FriendFeed OAuth")
+ return dict(
+ key=self.settings["friendfeed_consumer_key"],
+ secret=self.settings["friendfeed_consumer_secret"])
+
+ def _oauth_get_user(self, access_token, callback):
+ callback = self.async_callback(self._parse_user_response, callback)
+ self.friendfeed_request(
+ "/feedinfo/" + access_token["username"],
+ include="id,name,description", access_token=access_token,
+ callback=callback)
+
+ def _parse_user_response(self, callback, user):
+ if user:
+ user["username"] = user["id"]
+ callback(user)
+
+
+class GoogleMixin(OpenIdMixin, OAuthMixin):
+ """Google Open ID / OAuth authentication.
+
+ No application registration is necessary to use Google for authentication
+ or to access Google resources on behalf of a user. To authenticate with
+ Google, redirect with authenticate_redirect(). On return, parse the
+ response with get_authenticated_user(). We send a dict containing the
+ values for the user, including 'email', 'name', and 'locale'.
+ Example usage:
+
+ class GoogleHandler(tornado.web.RequestHandler, tornado.auth.GoogleMixin):
+ @tornado.web.asynchronous
+ def get(self):
+ if self.get_argument("openid.mode", None):
+ self.get_authenticated_user(self.async_callback(self._on_auth))
+ return
+ self.authenticate_redirect()
+
+ def _on_auth(self, user):
+ if not user:
+ raise tornado.web.HTTPError(500, "Google auth failed")
+ # Save the user with, e.g., set_secure_cookie()
+
+ """
+ _OPENID_ENDPOINT = "https://www.google.com/accounts/o8/ud"
+ _OAUTH_ACCESS_TOKEN_URL = "https://www.google.com/accounts/OAuthGetAccessToken"
+
+ def authorize_redirect(self, oauth_scope, callback_uri=None,
+ ax_attrs=["name","email","language","username"]):
+ """Authenticates and authorizes for the given Google resource.
+
+ Some of the available resources are:
+
+ Gmail Contacts - http://www.google.com/m8/feeds/
+ Calendar - http://www.google.com/calendar/feeds/
+ Finance - http://finance.google.com/finance/feeds/
+
+ You can authorize multiple resources by separating the resource
+ URLs with a space.
+ """
+ callback_uri = callback_uri or self.request.path
+ args = self._openid_args(callback_uri, ax_attrs=ax_attrs,
+ oauth_scope=oauth_scope)
+ self.redirect(self._OPENID_ENDPOINT + "?" + urllib.urlencode(args))
+
+ def get_authenticated_user(self, callback):
+ """Fetches the authenticated user data upon redirect."""
+ # Look to see if we are doing combined OpenID/OAuth
+ oauth_ns = ""
+ for name, values in self.request.arguments.iteritems():
+ if name.startswith("openid.ns.") and \
+ values[-1] == u"http://specs.openid.net/extensions/oauth/1.0":
+ oauth_ns = name[10:]
+ break
+ token = self.get_argument("openid." + oauth_ns + ".request_token", "")
+ if token:
+ http = httpclient.AsyncHTTPClient()
+ token = dict(key=token, secret="")
+ http.fetch(self._oauth_access_token_url(token),
+ self.async_callback(self._on_access_token, callback))
+ else:
+ OpenIdMixin.get_authenticated_user(self, callback)
+
+ def _oauth_consumer_token(self):
+ self.require_setting("google_consumer_key", "Google OAuth")
+ self.require_setting("google_consumer_secret", "Google OAuth")
+ return dict(
+ key=self.settings["google_consumer_key"],
+ secret=self.settings["google_consumer_secret"])
+
+ def _oauth_get_user(self, access_token, callback):
+ OpenIdMixin.get_authenticated_user(self, callback)
+
+
+class FacebookMixin(object):
+ """Facebook Connect authentication.
+
+ To authenticate with Facebook, register your application with
+ Facebook at http://www.facebook.com/developers/apps.php. Then
+ copy your API Key and Application Secret to the application settings
+ 'facebook_api_key' and 'facebook_secret'.
+
+ When your application is set up, you can use this Mixin like this
+ to authenticate the user with Facebook:
+
+ class FacebookHandler(tornado.web.RequestHandler,
+ tornado.auth.FacebookMixin):
+ @tornado.web.asynchronous
+ def get(self):
+ if self.get_argument("session", None):
+ self.get_authenticated_user(self.async_callback(self._on_auth))
+ return
+ self.authenticate_redirect()
+
+ def _on_auth(self, user):
+ if not user:
+ raise tornado.web.HTTPError(500, "Facebook auth failed")
+ # Save the user using, e.g., set_secure_cookie()
+
+ The user object returned by get_authenticated_user() includes the
+ attributes 'facebook_uid' and 'name' in addition to session attributes
+ like 'session_key'. You should save the session key with the user; it is
+ required to make requests on behalf of the user later with
+ facebook_request().
+ """
+ def authenticate_redirect(self, callback_uri=None, cancel_uri=None,
+ extended_permissions=None):
+ """Authenticates/installs this app for the current user."""
+ self.require_setting("facebook_api_key", "Facebook Connect")
+ callback_uri = callback_uri or self.request.path
+ args = {
+ "api_key": self.settings["facebook_api_key"],
+ "v": "1.0",
+ "fbconnect": "true",
+ "display": "page",
+ "next": urlparse.urljoin(self.request.full_url(), callback_uri),
+ "return_session": "true",
+ }
+ if cancel_uri:
+ args["cancel_url"] = urlparse.urljoin(
+ self.request.full_url(), cancel_uri)
+ if extended_permissions:
+ if isinstance(extended_permissions, basestring):
+ extended_permissions = [extended_permissions]
+ args["req_perms"] = ",".join(extended_permissions)
+ self.redirect("http://www.facebook.com/login.php?" +
+ urllib.urlencode(args))
+
+ def authorize_redirect(self, extended_permissions, callback_uri=None,
+ cancel_uri=None):
+ """Redirects to an authorization request for the given FB resource.
+
+ The available resource names are listed at
+ http://wiki.developers.facebook.com/index.php/Extended_permission.
+ The most common resource types include:
+
+ publish_stream
+ read_stream
+ email
+ sms
+
+ extended_permissions can be a single permission name or a list of
+ names. To get the session secret and session key, call
+ get_authenticated_user() just as you would with
+ authenticate_redirect().
+ """
+ self.authenticate_redirect(callback_uri, cancel_uri,
+ extended_permissions)
+
+ def get_authenticated_user(self, callback):
+ """Fetches the authenticated Facebook user.
+
+ The authenticated user includes the special Facebook attributes
+ 'session_key' and 'facebook_uid' in addition to the standard
+ user attributes like 'name'.
+ """
+ self.require_setting("facebook_api_key", "Facebook Connect")
+ session = escape.json_decode(self.get_argument("session"))
+ self.facebook_request(
+ method="facebook.users.getInfo",
+ callback=self.async_callback(
+ self._on_get_user_info, callback, session),
+ session_key=session["session_key"],
+ uids=session["uid"],
+ fields="uid,first_name,last_name,name,locale,pic_square," \
+ "profile_url,username")
+
+ def facebook_request(self, method, callback, **args):
+ """Makes a Facebook API REST request.
+
+ We automatically include the Facebook API key and signature, but
+ it is the callers responsibility to include 'session_key' and any
+ other required arguments to the method.
+
+ The available Facebook methods are documented here:
+ http://wiki.developers.facebook.com/index.php/API
+
+ Here is an example for the stream.get() method:
+
+ class MainHandler(tornado.web.RequestHandler,
+ tornado.auth.FacebookMixin):
+ @tornado.web.authenticated
+ @tornado.web.asynchronous
+ def get(self):
+ self.facebook_request(
+ method="stream.get",
+ callback=self.async_callback(self._on_stream),
+ session_key=self.current_user["session_key"])
+
+ def _on_stream(self, stream):
+ if stream is None:
+ # Not authorized to read the stream yet?
+ self.redirect(self.authorize_redirect("read_stream"))
+ return
+ self.render("stream.html", stream=stream)
+
+ """
+ self.require_setting("facebook_api_key", "Facebook Connect")
+ self.require_setting("facebook_secret", "Facebook Connect")
+ if not method.startswith("facebook."):
+ method = "facebook." + method
+ args["api_key"] = self.settings["facebook_api_key"]
+ args["v"] = "1.0"
+ args["method"] = method
+ args["call_id"] = str(long(time.time() * 1e6))
+ args["format"] = "json"
+ args["sig"] = self._signature(args)
+ url = "http://api.facebook.com/restserver.php?" + \
+ urllib.urlencode(args)
+ http = httpclient.AsyncHTTPClient()
+ http.fetch(url, callback=self.async_callback(
+ self._parse_response, callback))
+
+ def _on_get_user_info(self, callback, session, users):
+ if users is None:
+ callback(None)
+ return
+ callback({
+ "name": users[0]["name"],
+ "first_name": users[0]["first_name"],
+ "last_name": users[0]["last_name"],
+ "uid": users[0]["uid"],
+ "locale": users[0]["locale"],
+ "pic_square": users[0]["pic_square"],
+ "profile_url": users[0]["profile_url"],
+ "username": users[0].get("username"),
+ "session_key": session["session_key"],
+ "session_expires": session["expires"],
+ })
+
+ def _parse_response(self, callback, response):
+ if response.error:
+ _log.warning("HTTP error from Facebook: %s", response.error)
+ callback(None)
+ return
+ try:
+ json = escape.json_decode(response.body)
+ except:
+ _log.warning("Invalid JSON from Facebook: %r", response.body)
+ callback(None)
+ return
+ if isinstance(json, dict) and json.get("error_code"):
+ _log.warning("Facebook error: %d: %r", json["error_code"],
+ json.get("error_msg"))
+ callback(None)
+ return
+ callback(json)
+
+ def _signature(self, args):
+ parts = ["%s=%s" % (n, args[n]) for n in sorted(args.keys())]
+ body = "".join(parts) + self.settings["facebook_secret"]
+ if isinstance(body, unicode): body = body.encode("utf-8")
+ return hashlib.md5(body).hexdigest()
+
+
+def _oauth_signature(consumer_token, method, url, parameters={}, token=None):
+ """Calculates the HMAC-SHA1 OAuth signature for the given request.
+
+ See http://oauth.net/core/1.0/#signing_process
+ """
+ parts = urlparse.urlparse(url)
+ scheme, netloc, path = parts[:3]
+ normalized_url = scheme.lower() + "://" + netloc.lower() + path
+
+ base_elems = []
+ base_elems.append(method.upper())
+ base_elems.append(normalized_url)
+ base_elems.append("&".join("%s=%s" % (k, _oauth_escape(str(v)))
+ for k, v in sorted(parameters.items())))
+ base_string = "&".join(_oauth_escape(e) for e in base_elems)
+
+ key_elems = [consumer_token["secret"]]
+ key_elems.append(token["secret"] if token else "")
+ key = "&".join(key_elems)
+
+ hash = hmac.new(key, base_string, hashlib.sha1)
+ return binascii.b2a_base64(hash.digest())[:-1]
+
+
+def _oauth_escape(val):
+ if isinstance(val, unicode):
+ val = val.encode("utf-8")
+ return urllib.quote(val, safe="~")
+
+
+def _oauth_parse_response(body):
+ p = cgi.parse_qs(body, keep_blank_values=False)
+ token = dict(key=p["oauth_token"][0], secret=p["oauth_token_secret"][0])
+
+ # Add the extra parameters the Provider included to the token
+ special = ("oauth_token", "oauth_token_secret")
+ token.update((k, p[k][0]) for k in p if k not in special)
+ return token
diff --git a/vendor/tornado/tornado/autoreload.py b/vendor/tornado/tornado/autoreload.py
new file mode 100644
index 0000000000..231cfe892c
--- /dev/null
+++ b/vendor/tornado/tornado/autoreload.py
@@ -0,0 +1,95 @@
+#!/usr/bin/env python
+#
+# Copyright 2009 Facebook
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""A module to automatically restart the server when a module is modified.
+
+This module depends on IOLoop, so it will not work in WSGI applications
+and Google AppEngine.
+"""
+
+import functools
+import errno
+import ioloop
+import logging
+import os
+import os.path
+import sys
+import types
+
+_log = logging.getLogger('tornado.autoreload')
+
+def start(io_loop=None, check_time=500):
+ """Restarts the process automatically when a module is modified.
+
+ We run on the I/O loop, and restarting is a destructive operation,
+ so will terminate any pending requests.
+ """
+ io_loop = io_loop or ioloop.IOLoop.instance()
+ modify_times = {}
+ callback = functools.partial(_reload_on_update, io_loop, modify_times)
+ scheduler = ioloop.PeriodicCallback(callback, check_time, io_loop=io_loop)
+ scheduler.start()
+
+
+_reload_attempted = False
+
+def _reload_on_update(io_loop, modify_times):
+ global _reload_attempted
+ if _reload_attempted:
+ # We already tried to reload and it didn't work, so don't try again.
+ return
+ for module in sys.modules.values():
+ # Some modules play games with sys.modules (e.g. email/__init__.py
+ # in the standard library), and occasionally this can cause strange
+ # failures in getattr. Just ignore anything that's not an ordinary
+ # module.
+ if not isinstance(module, types.ModuleType): continue
+ path = getattr(module, "__file__", None)
+ if not path: continue
+ if path.endswith(".pyc") or path.endswith(".pyo"):
+ path = path[:-1]
+ try:
+ modified = os.stat(path).st_mtime
+ except:
+ continue
+ if path not in modify_times:
+ modify_times[path] = modified
+ continue
+ if modify_times[path] != modified:
+ _log.info("%s modified; restarting server", path)
+ _reload_attempted = True
+ for fd in io_loop._handlers.keys():
+ try:
+ os.close(fd)
+ except:
+ pass
+ try:
+ os.execv(sys.executable, [sys.executable] + sys.argv)
+ except OSError, e:
+ # Mac OS X versions prior to 10.6 do not support execv in
+ # a process that contains multiple threads. Instead of
+ # re-executing in the current process, start a new one
+ # and cause the current process to exit. This isn't
+ # ideal since the new process is detached from the parent
+ # terminal and thus cannot easily be killed with ctrl-C,
+ # but it's better than not being able to autoreload at
+ # all.
+ # Unfortunately the errno returned in this case does not
+ # appear to be consistent, so we can't easily check for
+ # this error specifically.
+ os.spawnv(os.P_NOWAIT, sys.executable,
+ [sys.executable] + sys.argv)
+ sys.exit(0)
diff --git a/vendor/tornado/tornado/database.py b/vendor/tornado/tornado/database.py
new file mode 100644
index 0000000000..3f78e00b94
--- /dev/null
+++ b/vendor/tornado/tornado/database.py
@@ -0,0 +1,180 @@
+#!/usr/bin/env python
+#
+# Copyright 2009 Facebook
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""A lightweight wrapper around MySQLdb."""
+
+import copy
+import MySQLdb
+import MySQLdb.constants
+import MySQLdb.converters
+import MySQLdb.cursors
+import itertools
+import logging
+
+_log = logging.getLogger('tornado.database')
+
+class Connection(object):
+ """A lightweight wrapper around MySQLdb DB-API connections.
+
+ The main value we provide is wrapping rows in a dict/object so that
+ columns can be accessed by name. Typical usage:
+
+ db = database.Connection("localhost", "mydatabase")
+ for article in db.query("SELECT * FROM articles"):
+ print article.title
+
+ Cursors are hidden by the implementation, but other than that, the methods
+ are very similar to the DB-API.
+
+ We explicitly set the timezone to UTC and the character encoding to
+ UTF-8 on all connections to avoid time zone and encoding errors.
+ """
+ def __init__(self, host, database, user=None, password=None):
+ self.host = host
+ self.database = database
+
+ args = dict(conv=CONVERSIONS, use_unicode=True, charset="utf8",
+ db=database, init_command='SET time_zone = "+0:00"',
+ sql_mode="TRADITIONAL")
+ if user is not None:
+ args["user"] = user
+ if password is not None:
+ args["passwd"] = password
+
+ # We accept a path to a MySQL socket file or a host(:port) string
+ if "/" in host:
+ args["unix_socket"] = host
+ else:
+ self.socket = None
+ pair = host.split(":")
+ if len(pair) == 2:
+ args["host"] = pair[0]
+ args["port"] = int(pair[1])
+ else:
+ args["host"] = host
+ args["port"] = 3306
+
+ self._db = None
+ self._db_args = args
+ try:
+ self.reconnect()
+ except:
+ _log.error("Cannot connect to MySQL on %s", self.host,
+ exc_info=True)
+
+ def __del__(self):
+ self.close()
+
+ def close(self):
+ """Closes this database connection."""
+ if getattr(self, "_db", None) is not None:
+ self._db.close()
+ self._db = None
+
+ def reconnect(self):
+ """Closes the existing database connection and re-opens it."""
+ self.close()
+ self._db = MySQLdb.connect(**self._db_args)
+ self._db.autocommit(True)
+
+ def iter(self, query, *parameters):
+ """Returns an iterator for the given query and parameters."""
+ if self._db is None: self.reconnect()
+ cursor = MySQLdb.cursors.SSCursor(self._db)
+ try:
+ self._execute(cursor, query, parameters)
+ column_names = [d[0] for d in cursor.description]
+ for row in cursor:
+ yield Row(zip(column_names, row))
+ finally:
+ cursor.close()
+
+ def query(self, query, *parameters):
+ """Returns a row list for the given query and parameters."""
+ cursor = self._cursor()
+ try:
+ self._execute(cursor, query, parameters)
+ column_names = [d[0] for d in cursor.description]
+ return [Row(itertools.izip(column_names, row)) for row in cursor]
+ finally:
+ cursor.close()
+
+ def get(self, query, *parameters):
+ """Returns the first row returned for the given query."""
+ rows = self.query(query, *parameters)
+ if not rows:
+ return None
+ elif len(rows) > 1:
+ raise Exception("Multiple rows returned for Database.get() query")
+ else:
+ return rows[0]
+
+ def execute(self, query, *parameters):
+ """Executes the given query, returning the lastrowid from the query."""
+ cursor = self._cursor()
+ try:
+ self._execute(cursor, query, parameters)
+ return cursor.lastrowid
+ finally:
+ cursor.close()
+
+ def executemany(self, query, parameters):
+ """Executes the given query against all the given param sequences.
+
+ We return the lastrowid from the query.
+ """
+ cursor = self._cursor()
+ try:
+ cursor.executemany(query, parameters)
+ return cursor.lastrowid
+ finally:
+ cursor.close()
+
+ def _cursor(self):
+ if self._db is None: self.reconnect()
+ return self._db.cursor()
+
+ def _execute(self, cursor, query, parameters):
+ try:
+ return cursor.execute(query, parameters)
+ except OperationalError:
+ _log.error("Error connecting to MySQL on %s", self.host)
+ self.close()
+ raise
+
+
+class Row(dict):
+ """A dict that allows for object-like property access syntax."""
+ def __getattr__(self, name):
+ try:
+ return self[name]
+ except KeyError:
+ raise AttributeError(name)
+
+
+# Fix the access conversions to properly recognize unicode/binary
+FIELD_TYPE = MySQLdb.constants.FIELD_TYPE
+FLAG = MySQLdb.constants.FLAG
+CONVERSIONS = copy.deepcopy(MySQLdb.converters.conversions)
+for field_type in \
+ [FIELD_TYPE.BLOB, FIELD_TYPE.STRING, FIELD_TYPE.VAR_STRING] + \
+ ([FIELD_TYPE.VARCHAR] if 'VARCHAR' in vars(FIELD_TYPE) else []):
+ CONVERSIONS[field_type].insert(0, (FLAG.BINARY, str))
+
+
+# Alias some common MySQL exceptions
+IntegrityError = MySQLdb.IntegrityError
+OperationalError = MySQLdb.OperationalError
diff --git a/vendor/tornado/tornado/epoll.c b/vendor/tornado/tornado/epoll.c
new file mode 100644
index 0000000000..9a2e3a3747
--- /dev/null
+++ b/vendor/tornado/tornado/epoll.c
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2009 Facebook
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License. You may obtain
+ * a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+#include "Python.h"
+#include <string.h>
+#include <sys/epoll.h>
+
+#define MAX_EVENTS 24
+
+/*
+ * Simple wrapper around epoll_create.
+ */
+static PyObject* _epoll_create(void) {
+ int fd = epoll_create(MAX_EVENTS);
+ if (fd == -1) {
+ PyErr_SetFromErrno(PyExc_Exception);
+ return NULL;
+ }
+
+ return PyInt_FromLong(fd);
+}
+
+/*
+ * Simple wrapper around epoll_ctl. We throw an exception if the call fails
+ * rather than returning the error code since it is an infrequent (and likely
+ * catastrophic) event when it does happen.
+ */
+static PyObject* _epoll_ctl(PyObject* self, PyObject* args) {
+ int epfd, op, fd, events;
+ struct epoll_event event;
+
+ if (!PyArg_ParseTuple(args, "iiiI", &epfd, &op, &fd, &events)) {
+ return NULL;
+ }
+
+ memset(&event, 0, sizeof(event));
+ event.events = events;
+ event.data.fd = fd;
+ if (epoll_ctl(epfd, op, fd, &event) == -1) {
+ PyErr_SetFromErrno(PyExc_OSError);
+ return NULL;
+ }
+
+ Py_INCREF(Py_None);
+ return Py_None;
+}
+
+/*
+ * Simple wrapper around epoll_wait. We return None if the call times out and
+ * throw an exception if an error occurs. Otherwise, we return a list of
+ * (fd, event) tuples.
+ */
+static PyObject* _epoll_wait(PyObject* self, PyObject* args) {
+ struct epoll_event events[MAX_EVENTS];
+ int epfd, timeout, num_events, i;
+ PyObject* list;
+ PyObject* tuple;
+
+ if (!PyArg_ParseTuple(args, "ii", &epfd, &timeout)) {
+ return NULL;
+ }
+
+ Py_BEGIN_ALLOW_THREADS
+ num_events = epoll_wait(epfd, events, MAX_EVENTS, timeout);
+ Py_END_ALLOW_THREADS
+ if (num_events == -1) {
+ PyErr_SetFromErrno(PyExc_Exception);
+ return NULL;
+ }
+
+ list = PyList_New(num_events);
+ for (i = 0; i < num_events; i++) {
+ tuple = PyTuple_New(2);
+ PyTuple_SET_ITEM(tuple, 0, PyInt_FromLong(events[i].data.fd));
+ PyTuple_SET_ITEM(tuple, 1, PyInt_FromLong(events[i].events));
+ PyList_SET_ITEM(list, i, tuple);
+ }
+ return list;
+}
+
+/*
+ * Our method declararations
+ */
+static PyMethodDef kEpollMethods[] = {
+ {"epoll_create", (PyCFunction)_epoll_create, METH_NOARGS,
+ "Create an epoll file descriptor"},
+ {"epoll_ctl", _epoll_ctl, METH_VARARGS,
+ "Control an epoll file descriptor"},
+ {"epoll_wait", _epoll_wait, METH_VARARGS,
+ "Wait for events on an epoll file descriptor"},
+ {NULL, NULL, 0, NULL}
+};
+
+/*
+ * Module initialization
+ */
+PyMODINIT_FUNC initepoll(void) {
+ Py_InitModule("epoll", kEpollMethods);
+}
diff --git a/vendor/tornado/tornado/escape.py b/vendor/tornado/tornado/escape.py
new file mode 100644
index 0000000000..bacb1c51d0
--- /dev/null
+++ b/vendor/tornado/tornado/escape.py
@@ -0,0 +1,112 @@
+#!/usr/bin/env python
+#
+# Copyright 2009 Facebook
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""Escaping/unescaping methods for HTML, JSON, URLs, and others."""
+
+import htmlentitydefs
+import re
+import xml.sax.saxutils
+import urllib
+
+try:
+ import json
+ assert hasattr(json, "loads") and hasattr(json, "dumps")
+ _json_decode = lambda s: json.loads(s)
+ _json_encode = lambda v: json.dumps(v)
+except:
+ try:
+ import simplejson
+ _json_decode = lambda s: simplejson.loads(_unicode(s))
+ _json_encode = lambda v: simplejson.dumps(v)
+ except ImportError:
+ try:
+ # For Google AppEngine
+ from django.utils import simplejson
+ _json_decode = lambda s: simplejson.loads(_unicode(s))
+ _json_encode = lambda v: simplejson.dumps(v)
+ except ImportError:
+ raise Exception("A JSON parser is required, e.g., simplejson at "
+ "http://pypi.python.org/pypi/simplejson/")
+
+
+def xhtml_escape(value):
+ """Escapes a string so it is valid within XML or XHTML."""
+ return utf8(xml.sax.saxutils.escape(value, {'"': "&quot;"}))
+
+
+def xhtml_unescape(value):
+ """Un-escapes an XML-escaped string."""
+ return re.sub(r"&(#?)(\w+?);", _convert_entity, _unicode(value))
+
+
+def json_encode(value):
+ """JSON-encodes the given Python object."""
+ return _json_encode(value)
+
+
+def json_decode(value):
+ """Returns Python objects for the given JSON string."""
+ return _json_decode(value)
+
+
+def squeeze(value):
+ """Replace all sequences of whitespace chars with a single space."""
+ return re.sub(r"[\x00-\x20]+", " ", value).strip()
+
+
+def url_escape(value):
+ """Returns a valid URL-encoded version of the given value."""
+ return urllib.quote_plus(utf8(value))
+
+
+def url_unescape(value):
+ """Decodes the given value from a URL."""
+ return _unicode(urllib.unquote_plus(value))
+
+
+def utf8(value):
+ if isinstance(value, unicode):
+ return value.encode("utf-8")
+ assert isinstance(value, str)
+ return value
+
+
+def _unicode(value):
+ if isinstance(value, str):
+ return value.decode("utf-8")
+ assert isinstance(value, unicode)
+ return value
+
+
+def _convert_entity(m):
+ if m.group(1) == "#":
+ try:
+ return unichr(int(m.group(2)))
+ except ValueError:
+ return "&#%s;" % m.group(2)
+ try:
+ return _HTML_UNICODE_MAP[m.group(2)]
+ except KeyError:
+ return "&%s;" % m.group(2)
+
+
+def _build_unicode_map():
+ unicode_map = {}
+ for name, value in htmlentitydefs.name2codepoint.iteritems():
+ unicode_map[name] = unichr(value)
+ return unicode_map
+
+_HTML_UNICODE_MAP = _build_unicode_map()
diff --git a/vendor/tornado/tornado/httpclient.py b/vendor/tornado/tornado/httpclient.py
new file mode 100644
index 0000000000..2c9155eb9e
--- /dev/null
+++ b/vendor/tornado/tornado/httpclient.py
@@ -0,0 +1,465 @@
+#!/usr/bin/env python
+#
+# Copyright 2009 Facebook
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""Blocking and non-blocking HTTP client implementations using pycurl."""
+
+import calendar
+import collections
+import cStringIO
+import email.utils
+import errno
+import functools
+import httplib
+import ioloop
+import logging
+import pycurl
+import time
+import weakref
+
+_log = logging.getLogger('tornado.httpclient')
+
+class HTTPClient(object):
+ """A blocking HTTP client backed with pycurl.
+
+ Typical usage looks like this:
+
+ http_client = httpclient.HTTPClient()
+ try:
+ response = http_client.fetch("http://www.google.com/")
+ print response.body
+ except httpclient.HTTPError, e:
+ print "Error:", e
+
+ fetch() can take a string URL or an HTTPRequest instance, which offers
+ more options, like executing POST/PUT/DELETE requests.
+ """
+ def __init__(self, max_simultaneous_connections=None):
+ self._curl = _curl_create(max_simultaneous_connections)
+
+ def __del__(self):
+ self._curl.close()
+
+ def fetch(self, request, **kwargs):
+ """Executes an HTTPRequest, returning an HTTPResponse.
+
+ If an error occurs during the fetch, we raise an HTTPError.
+ """
+ if not isinstance(request, HTTPRequest):
+ request = HTTPRequest(url=request, **kwargs)
+ buffer = cStringIO.StringIO()
+ headers = {}
+ try:
+ _curl_setup_request(self._curl, request, buffer, headers)
+ self._curl.perform()
+ code = self._curl.getinfo(pycurl.HTTP_CODE)
+ if code < 200 or code >= 300:
+ raise HTTPError(code)
+ effective_url = self._curl.getinfo(pycurl.EFFECTIVE_URL)
+ return HTTPResponse(
+ request=request, code=code, headers=headers,
+ body=buffer.getvalue(), effective_url=effective_url)
+ except pycurl.error, e:
+ raise CurlError(*e)
+ finally:
+ buffer.close()
+
+
+class AsyncHTTPClient(object):
+ """An non-blocking HTTP client backed with pycurl.
+
+ Example usage:
+
+ import ioloop
+
+ def handle_request(response):
+ if response.error:
+ print "Error:", response.error
+ else:
+ print response.body
+ ioloop.IOLoop.instance().stop()
+
+ http_client = httpclient.AsyncHTTPClient()
+ http_client.fetch("http://www.google.com/", handle_request)
+ ioloop.IOLoop.instance().start()
+
+ fetch() can take a string URL or an HTTPRequest instance, which offers
+ more options, like executing POST/PUT/DELETE requests.
+
+ The keyword argument max_clients to the AsyncHTTPClient constructor
+ determines the maximum number of simultaneous fetch() operations that
+ can execute in parallel on each IOLoop.
+ """
+ _ASYNC_CLIENTS = weakref.WeakKeyDictionary()
+
+ def __new__(cls, io_loop=None, max_clients=10,
+ max_simultaneous_connections=None):
+ # There is one client per IOLoop since they share curl instances
+ io_loop = io_loop or ioloop.IOLoop.instance()
+ if io_loop in cls._ASYNC_CLIENTS:
+ return cls._ASYNC_CLIENTS[io_loop]
+ else:
+ instance = super(AsyncHTTPClient, cls).__new__(cls)
+ instance.io_loop = io_loop
+ instance._multi = pycurl.CurlMulti()
+ instance._curls = [_curl_create(max_simultaneous_connections)
+ for i in xrange(max_clients)]
+ instance._free_list = instance._curls[:]
+ instance._requests = collections.deque()
+ instance._fds = {}
+ instance._events = {}
+ instance._added_perform_callback = False
+ instance._timeout = None
+ cls._ASYNC_CLIENTS[io_loop] = instance
+ return instance
+
+ def close(self):
+ """Destroys this http client, freeing any file descriptors used.
+ Not needed in normal use, but may be helpful in unittests that
+ create and destroy http clients. No other methods may be called
+ on the AsyncHTTPClient after close().
+ """
+ del AsyncHTTPClient._ASYNC_CLIENTS[self.io_loop]
+ for curl in self._curls:
+ curl.close()
+ self._multi.close()
+
+ def fetch(self, request, callback, **kwargs):
+ """Executes an HTTPRequest, calling callback with an HTTPResponse.
+
+ If an error occurs during the fetch, the HTTPResponse given to the
+ callback has a non-None error attribute that contains the exception
+ encountered during the request. You can call response.reraise() to
+ throw the exception (if any) in the callback.
+ """
+ if not isinstance(request, HTTPRequest):
+ request = HTTPRequest(url=request, **kwargs)
+ self._requests.append((request, callback))
+ self._add_perform_callback()
+
+ def _add_perform_callback(self):
+ if not self._added_perform_callback:
+ self.io_loop.add_callback(self._perform)
+ self._added_perform_callback = True
+
+ def _handle_events(self, fd, events):
+ self._events[fd] = events
+ self._add_perform_callback()
+
+ def _handle_timeout(self):
+ self._timeout = None
+ self._perform()
+
+ def _perform(self):
+ self._added_perform_callback = False
+
+ while True:
+ while True:
+ ret, num_handles = self._multi.perform()
+ if ret != pycurl.E_CALL_MULTI_PERFORM:
+ break
+
+ # Handle completed fetches
+ completed = 0
+ while True:
+ num_q, ok_list, err_list = self._multi.info_read()
+ for curl in ok_list:
+ self._finish(curl)
+ completed += 1
+ for curl, errnum, errmsg in err_list:
+ self._finish(curl, errnum, errmsg)
+ completed += 1
+ if num_q == 0:
+ break
+
+ # Start fetching new URLs
+ started = 0
+ while self._free_list and self._requests:
+ started += 1
+ curl = self._free_list.pop()
+ (request, callback) = self._requests.popleft()
+ curl.info = {
+ "headers": {},
+ "buffer": cStringIO.StringIO(),
+ "request": request,
+ "callback": callback,
+ "start_time": time.time(),
+ }
+ _curl_setup_request(curl, request, curl.info["buffer"],
+ curl.info["headers"])
+ self._multi.add_handle(curl)
+
+ if not started and not completed:
+ break
+
+ if self._timeout is not None:
+ self.io_loop.remove_timeout(self._timeout)
+ self._timeout = None
+
+ if num_handles:
+ self._timeout = self.io_loop.add_timeout(
+ time.time() + 0.2, self._handle_timeout)
+
+ # Wait for more I/O
+ fds = {}
+ (readable, writable, exceptable) = self._multi.fdset()
+ for fd in readable:
+ fds[fd] = fds.get(fd, 0) | 0x1 | 0x2
+ for fd in writable:
+ fds[fd] = fds.get(fd, 0) | 0x4
+ for fd in exceptable:
+ fds[fd] = fds.get(fd, 0) | 0x8 | 0x10
+
+ for fd in self._fds:
+ if fd not in fds:
+ try:
+ self.io_loop.remove_handler(fd)
+ except (OSError, IOError), e:
+ if e[0] != errno.ENOENT:
+ raise
+
+ for fd, events in fds.iteritems():
+ old_events = self._fds.get(fd, None)
+ if old_events is None:
+ self.io_loop.add_handler(fd, self._handle_events, events)
+ elif old_events != events:
+ try:
+ self.io_loop.update_handler(fd, events)
+ except (OSError, IOError), e:
+ if e[0] == errno.ENOENT:
+ self.io_loop.add_handler(fd, self._handle_events,
+ events)
+ else:
+ raise
+ self._fds = fds
+
+ def _finish(self, curl, curl_error=None, curl_message=None):
+ info = curl.info
+ curl.info = None
+ self._multi.remove_handle(curl)
+ self._free_list.append(curl)
+ if curl_error:
+ error = CurlError(curl_error, curl_message)
+ code = error.code
+ body = None
+ effective_url = None
+ else:
+ error = None
+ code = curl.getinfo(pycurl.HTTP_CODE)
+ body = info["buffer"].getvalue()
+ effective_url = curl.getinfo(pycurl.EFFECTIVE_URL)
+ info["buffer"].close()
+ info["callback"](HTTPResponse(
+ request=info["request"], code=code, headers=info["headers"],
+ body=body, effective_url=effective_url, error=error,
+ request_time=time.time() - info["start_time"]))
+
+
+class HTTPRequest(object):
+ def __init__(self, url, method="GET", headers={}, body=None,
+ auth_username=None, auth_password=None,
+ connect_timeout=20.0, request_timeout=20.0,
+ if_modified_since=None, follow_redirects=True,
+ max_redirects=5, user_agent=None, use_gzip=True,
+ network_interface=None, streaming_callback=None,
+ header_callback=None, prepare_curl_callback=None):
+ if if_modified_since:
+ timestamp = calendar.timegm(if_modified_since.utctimetuple())
+ headers["If-Modified-Since"] = email.utils.formatdate(
+ timestamp, localtime=False, usegmt=True)
+ if "Pragma" not in headers:
+ headers["Pragma"] = ""
+ self.url = _utf8(url)
+ self.method = method
+ self.headers = headers
+ self.body = body
+ self.auth_username = _utf8(auth_username)
+ self.auth_password = _utf8(auth_password)
+ self.connect_timeout = connect_timeout
+ self.request_timeout = request_timeout
+ self.follow_redirects = follow_redirects
+ self.max_redirects = max_redirects
+ self.user_agent = user_agent
+ self.use_gzip = use_gzip
+ self.network_interface = network_interface
+ self.streaming_callback = streaming_callback
+ self.header_callback = header_callback
+ self.prepare_curl_callback = prepare_curl_callback
+
+
+class HTTPResponse(object):
+ def __init__(self, request, code, headers={}, body="", effective_url=None,
+ error=None, request_time=None):
+ self.request = request
+ self.code = code
+ self.headers = headers
+ self.body = body
+ if effective_url is None:
+ self.effective_url = request.url
+ else:
+ self.effective_url = effective_url
+ if error is None:
+ if self.code < 200 or self.code >= 300:
+ self.error = HTTPError(self.code)
+ else:
+ self.error = None
+ else:
+ self.error = error
+ self.request_time = request_time
+
+ def rethrow(self):
+ if self.error:
+ raise self.error
+
+ def __repr__(self):
+ args = ",".join("%s=%r" % i for i in self.__dict__.iteritems())
+ return "%s(%s)" % (self.__class__.__name__, args)
+
+
+class HTTPError(Exception):
+ def __init__(self, code, message=None):
+ self.code = code
+ message = message or httplib.responses.get(code, "Unknown")
+ Exception.__init__(self, "HTTP %d: %s" % (self.code, message))
+
+
+class CurlError(HTTPError):
+ def __init__(self, errno, message):
+ HTTPError.__init__(self, 599, message)
+ self.errno = errno
+
+
+def _curl_create(max_simultaneous_connections=None):
+ curl = pycurl.Curl()
+ if _log.isEnabledFor(logging.DEBUG):
+ curl.setopt(pycurl.VERBOSE, 1)
+ curl.setopt(pycurl.DEBUGFUNCTION, _curl_debug)
+ curl.setopt(pycurl.MAXCONNECTS, max_simultaneous_connections or 5)
+ return curl
+
+
+def _curl_setup_request(curl, request, buffer, headers):
+ curl.setopt(pycurl.URL, request.url)
+ curl.setopt(pycurl.HTTPHEADER,
+ ["%s: %s" % i for i in request.headers.iteritems()])
+ try:
+ if request.header_callback:
+ curl.setopt(pycurl.HEADERFUNCTION, request.header_callback)
+ else:
+ curl.setopt(pycurl.HEADERFUNCTION,
+ functools.partial(_curl_header_callback, headers))
+ except:
+ # Old version of curl; response will not include headers
+ pass
+ if request.streaming_callback:
+ curl.setopt(pycurl.WRITEFUNCTION, request.streaming_callback)
+ else:
+ curl.setopt(pycurl.WRITEFUNCTION, buffer.write)
+ curl.setopt(pycurl.FOLLOWLOCATION, request.follow_redirects)
+ curl.setopt(pycurl.MAXREDIRS, request.max_redirects)
+ curl.setopt(pycurl.CONNECTTIMEOUT, int(request.connect_timeout))
+ curl.setopt(pycurl.TIMEOUT, int(request.request_timeout))
+ if request.user_agent:
+ curl.setopt(pycurl.USERAGENT, request.user_agent)
+ else:
+ curl.setopt(pycurl.USERAGENT, "Mozilla/5.0 (compatible; pycurl)")
+ if request.network_interface:
+ curl.setopt(pycurl.INTERFACE, request.network_interface)
+ if request.use_gzip:
+ curl.setopt(pycurl.ENCODING, "gzip,deflate")
+ else:
+ curl.setopt(pycurl.ENCODING, "none")
+
+ # Set the request method through curl's retarded interface which makes
+ # up names for almost every single method
+ curl_options = {
+ "GET": pycurl.HTTPGET,
+ "POST": pycurl.POST,
+ "PUT": pycurl.UPLOAD,
+ "HEAD": pycurl.NOBODY,
+ }
+ custom_methods = set(["DELETE"])
+ for o in curl_options.values():
+ curl.setopt(o, False)
+ if request.method in curl_options:
+ curl.unsetopt(pycurl.CUSTOMREQUEST)
+ curl.setopt(curl_options[request.method], True)
+ elif request.method in custom_methods:
+ curl.setopt(pycurl.CUSTOMREQUEST, request.method)
+ else:
+ raise KeyError('unknown method ' + request.method)
+
+ # Handle curl's cryptic options for every individual HTTP method
+ if request.method in ("POST", "PUT"):
+ request_buffer = cStringIO.StringIO(request.body)
+ curl.setopt(pycurl.READFUNCTION, request_buffer.read)
+ if request.method == "POST":
+ def ioctl(cmd):
+ if cmd == curl.IOCMD_RESTARTREAD:
+ request_buffer.seek(0)
+ curl.setopt(pycurl.IOCTLFUNCTION, ioctl)
+ curl.setopt(pycurl.POSTFIELDSIZE, len(request.body))
+ else:
+ curl.setopt(pycurl.INFILESIZE, len(request.body))
+
+ if request.auth_username and request.auth_password:
+ userpwd = "%s:%s" % (request.auth_username, request.auth_password)
+ curl.setopt(pycurl.HTTPAUTH, pycurl.HTTPAUTH_BASIC)
+ curl.setopt(pycurl.USERPWD, userpwd)
+ _log.info("%s %s (username: %r)", request.method, request.url,
+ request.auth_username)
+ else:
+ curl.unsetopt(pycurl.USERPWD)
+ _log.info("%s %s", request.method, request.url)
+ if request.prepare_curl_callback is not None:
+ request.prepare_curl_callback(curl)
+
+
+def _curl_header_callback(headers, header_line):
+ if header_line.startswith("HTTP/"):
+ headers.clear()
+ return
+ if header_line == "\r\n":
+ return
+ parts = header_line.split(": ")
+ if len(parts) != 2:
+ _log.warning("Invalid HTTP response header line %r", header_line)
+ return
+ name = parts[0].strip()
+ value = parts[1].strip()
+ if name in headers:
+ headers[name] = headers[name] + ',' + value
+ else:
+ headers[name] = value
+
+
+def _curl_debug(debug_type, debug_msg):
+ debug_types = ('I', '<', '>', '<', '>')
+ if debug_type == 0:
+ _log.debug('%s', debug_msg.strip())
+ elif debug_type in (1, 2):
+ for line in debug_msg.splitlines():
+ _log.debug('%s %s', debug_types[debug_type], line)
+ elif debug_type == 4:
+ _log.debug('%s %r', debug_types[debug_type], debug_msg)
+
+
+def _utf8(value):
+ if value is None:
+ return value
+ if isinstance(value, unicode):
+ return value.encode("utf-8")
+ assert isinstance(value, str)
+ return value
diff --git a/vendor/tornado/tornado/httpserver.py b/vendor/tornado/tornado/httpserver.py
new file mode 100644
index 0000000000..a7ec57eec4
--- /dev/null
+++ b/vendor/tornado/tornado/httpserver.py
@@ -0,0 +1,450 @@
+#!/usr/bin/env python
+#
+# Copyright 2009 Facebook
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""A non-blocking, single-threaded HTTP server."""
+
+import cgi
+import errno
+import functools
+import ioloop
+import iostream
+import logging
+import os
+import socket
+import time
+import urlparse
+
+try:
+ import fcntl
+except ImportError:
+ if os.name == 'nt':
+ import win32_support as fcntl
+ else:
+ raise
+
+try:
+ import ssl # Python 2.6+
+except ImportError:
+ ssl = None
+
+_log = logging.getLogger('tornado.httpserver')
+
+class HTTPServer(object):
+ """A non-blocking, single-threaded HTTP server.
+
+ A server is defined by a request callback that takes an HTTPRequest
+ instance as an argument and writes a valid HTTP response with
+ request.write(). request.finish() finishes the request (but does not
+ necessarily close the connection in the case of HTTP/1.1 keep-alive
+ requests). A simple example server that echoes back the URI you
+ requested:
+
+ import httpserver
+ import ioloop
+
+ def handle_request(request):
+ message = "You requested %s\n" % request.uri
+ request.write("HTTP/1.1 200 OK\r\nContent-Length: %d\r\n\r\n%s" % (
+ len(message), message))
+ request.finish()
+
+ http_server = httpserver.HTTPServer(handle_request)
+ http_server.listen(8888)
+ ioloop.IOLoop.instance().start()
+
+ HTTPServer is a very basic connection handler. Beyond parsing the
+ HTTP request body and headers, the only HTTP semantics implemented
+ in HTTPServer is HTTP/1.1 keep-alive connections. We do not, however,
+ implement chunked encoding, so the request callback must provide a
+ Content-Length header or implement chunked encoding for HTTP/1.1
+ requests for the server to run correctly for HTTP/1.1 clients. If
+ the request handler is unable to do this, you can provide the
+ no_keep_alive argument to the HTTPServer constructor, which will
+ ensure the connection is closed on every request no matter what HTTP
+ version the client is using.
+
+ If xheaders is True, we support the X-Real-Ip and X-Scheme headers,
+ which override the remote IP and HTTP scheme for all requests. These
+ headers are useful when running Tornado behind a reverse proxy or
+ load balancer.
+
+ HTTPServer can serve HTTPS (SSL) traffic with Python 2.6+ and OpenSSL.
+ To make this server serve SSL traffic, send the ssl_options dictionary
+ argument with the arguments required for the ssl.wrap_socket() method,
+ including "certfile" and "keyfile":
+
+ HTTPServer(applicaton, ssl_options={
+ "certfile": os.path.join(data_dir, "mydomain.crt"),
+ "keyfile": os.path.join(data_dir, "mydomain.key"),
+ })
+
+ By default, listen() runs in a single thread in a single process. You
+ can utilize all available CPUs on this machine by calling bind() and
+ start() instead of listen():
+
+ http_server = httpserver.HTTPServer(handle_request)
+ http_server.bind(8888)
+ http_server.start() # Forks multiple sub-processes
+ ioloop.IOLoop.instance().start()
+
+ start() detects the number of CPUs on this machine and "pre-forks" that
+ number of child processes so that we have one Tornado process per CPU,
+ all with their own IOLoop. You can also pass in the specific number of
+ child processes you want to run with if you want to override this
+ auto-detection.
+ """
+ def __init__(self, request_callback, no_keep_alive=False, io_loop=None,
+ xheaders=False, ssl_options=None):
+ """Initializes the server with the given request callback.
+
+ If you use pre-forking/start() instead of the listen() method to
+ start your server, you should not pass an IOLoop instance to this
+ constructor. Each pre-forked child process will create its own
+ IOLoop instance after the forking process.
+ """
+ self.request_callback = request_callback
+ self.no_keep_alive = no_keep_alive
+ self.io_loop = io_loop
+ self.xheaders = xheaders
+ self.ssl_options = ssl_options
+ self._socket = None
+ self._started = False
+
+ def listen(self, port, address=""):
+ """Binds to the given port and starts the server in a single process.
+
+ This method is a shortcut for:
+
+ server.bind(port, address)
+ server.start(1)
+
+ """
+ self.bind(port, address)
+ self.start(1)
+
+ def bind(self, port, address=""):
+ """Binds this server to the given port on the given IP address.
+
+ To start the server, call start(). If you want to run this server
+ in a single process, you can call listen() as a shortcut to the
+ sequence of bind() and start() calls.
+ """
+ assert not self._socket
+ self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
+ flags = fcntl.fcntl(self._socket.fileno(), fcntl.F_GETFD)
+ flags |= fcntl.FD_CLOEXEC
+ fcntl.fcntl(self._socket.fileno(), fcntl.F_SETFD, flags)
+ self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ self._socket.setblocking(0)
+ self._socket.bind((address, port))
+ self._socket.listen(128)
+
+ def start(self, num_processes=None):
+ """Starts this server in the IOLoop.
+
+ By default, we detect the number of cores available on this machine
+ and fork that number of child processes. If num_processes is given, we
+ fork that specific number of sub-processes.
+
+ If num_processes is 1 or we detect only 1 CPU core, we run the server
+ in this process and do not fork any additional child process.
+
+ Since we run use processes and not threads, there is no shared memory
+ between any server code.
+ """
+ assert not self._started
+ self._started = True
+ if num_processes is None:
+ # Use sysconf to detect the number of CPUs (cores)
+ try:
+ num_processes = os.sysconf("SC_NPROCESSORS_CONF")
+ except ValueError:
+ _log.error("Could not get num processors from sysconf; "
+ "running with one process")
+ num_processes = 1
+ if num_processes > 1 and ioloop.IOLoop.initialized():
+ _log.error("Cannot run in multiple processes: IOLoop instance "
+ "has already been initialized. You cannot call "
+ "IOLoop.instance() before calling start()")
+ num_processes = 1
+ if num_processes > 1:
+ _log.info("Pre-forking %d server processes", num_processes)
+ for i in range(num_processes):
+ if os.fork() == 0:
+ self.io_loop = ioloop.IOLoop.instance()
+ self.io_loop.add_handler(
+ self._socket.fileno(), self._handle_events,
+ ioloop.IOLoop.READ)
+ return
+ os.waitpid(-1, 0)
+ else:
+ if not self.io_loop:
+ self.io_loop = ioloop.IOLoop.instance()
+ self.io_loop.add_handler(self._socket.fileno(),
+ self._handle_events,
+ ioloop.IOLoop.READ)
+
+ def stop(self):
+ self.io_loop.remove_handler(self._socket.fileno())
+ self._socket.close()
+
+ def _handle_events(self, fd, events):
+ while True:
+ try:
+ connection, address = self._socket.accept()
+ except socket.error, e:
+ if e[0] in (errno.EWOULDBLOCK, errno.EAGAIN):
+ return
+ raise
+ if self.ssl_options is not None:
+ assert ssl, "Python 2.6+ and OpenSSL required for SSL"
+ connection = ssl.wrap_socket(
+ connection, server_side=True, **self.ssl_options)
+ try:
+ stream = iostream.IOStream(connection, io_loop=self.io_loop)
+ HTTPConnection(stream, address, self.request_callback,
+ self.no_keep_alive, self.xheaders)
+ except:
+ _log.error("Error in connection callback", exc_info=True)
+
+
+class HTTPConnection(object):
+ """Handles a connection to an HTTP client, executing HTTP requests.
+
+ We parse HTTP headers and bodies, and execute the request callback
+ until the HTTP conection is closed.
+ """
+ def __init__(self, stream, address, request_callback, no_keep_alive=False,
+ xheaders=False):
+ self.stream = stream
+ self.address = address
+ self.request_callback = request_callback
+ self.no_keep_alive = no_keep_alive
+ self.xheaders = xheaders
+ self._request = None
+ self._request_finished = False
+ self.stream.read_until("\r\n\r\n", self._on_headers)
+
+ def write(self, chunk):
+ assert self._request, "Request closed"
+ if not self.stream.closed():
+ self.stream.write(chunk, self._on_write_complete)
+
+ def finish(self):
+ assert self._request, "Request closed"
+ self._request_finished = True
+ if not self.stream.writing():
+ self._finish_request()
+
+ def _on_write_complete(self):
+ if self._request_finished:
+ self._finish_request()
+
+ def _finish_request(self):
+ if self.no_keep_alive:
+ disconnect = True
+ else:
+ connection_header = self._request.headers.get("Connection")
+ if self._request.supports_http_1_1():
+ disconnect = connection_header == "close"
+ elif ("Content-Length" in self._request.headers
+ or self._request.method in ("HEAD", "GET")):
+ disconnect = connection_header != "Keep-Alive"
+ else:
+ disconnect = True
+ self._request = None
+ self._request_finished = False
+ if disconnect:
+ self.stream.close()
+ return
+ self.stream.read_until("\r\n\r\n", self._on_headers)
+
+ def _on_headers(self, data):
+ eol = data.find("\r\n")
+ start_line = data[:eol]
+ method, uri, version = start_line.split(" ")
+ if not version.startswith("HTTP/"):
+ raise Exception("Malformed HTTP version in HTTP Request-Line")
+ headers = HTTPHeaders.parse(data[eol:])
+ self._request = HTTPRequest(
+ connection=self, method=method, uri=uri, version=version,
+ headers=headers, remote_ip=self.address[0])
+
+ content_length = headers.get("Content-Length")
+ if content_length:
+ content_length = int(content_length)
+ if content_length > self.stream.max_buffer_size:
+ raise Exception("Content-Length too long")
+ if headers.get("Expect") == "100-continue":
+ self.stream.write("HTTP/1.1 100 (Continue)\r\n\r\n")
+ self.stream.read_bytes(content_length, self._on_request_body)
+ return
+
+ self.request_callback(self._request)
+
+ def _on_request_body(self, data):
+ self._request.body = data
+ content_type = self._request.headers.get("Content-Type", "")
+ if self._request.method == "POST":
+ if content_type.startswith("application/x-www-form-urlencoded"):
+ arguments = cgi.parse_qs(self._request.body)
+ for name, values in arguments.iteritems():
+ values = [v for v in values if v]
+ if values:
+ self._request.arguments.setdefault(name, []).extend(
+ values)
+ elif content_type.startswith("multipart/form-data"):
+ boundary = content_type[30:]
+ if boundary: self._parse_mime_body(boundary, data)
+ self.request_callback(self._request)
+
+ def _parse_mime_body(self, boundary, data):
+ if data.endswith("\r\n"):
+ footer_length = len(boundary) + 6
+ else:
+ footer_length = len(boundary) + 4
+ parts = data[:-footer_length].split("--" + boundary + "\r\n")
+ for part in parts:
+ if not part: continue
+ eoh = part.find("\r\n\r\n")
+ if eoh == -1:
+ _log.warning("multipart/form-data missing headers")
+ continue
+ headers = HTTPHeaders.parse(part[:eoh])
+ name_header = headers.get("Content-Disposition", "")
+ if not name_header.startswith("form-data;") or \
+ not part.endswith("\r\n"):
+ _log.warning("Invalid multipart/form-data")
+ continue
+ value = part[eoh + 4:-2]
+ name_values = {}
+ for name_part in name_header[10:].split(";"):
+ name, name_value = name_part.strip().split("=", 1)
+ name_values[name] = name_value.strip('"').decode("utf-8")
+ if not name_values.get("name"):
+ _log.warning("multipart/form-data value missing name")
+ continue
+ name = name_values["name"]
+ if name_values.get("filename"):
+ ctype = headers.get("Content-Type", "application/unknown")
+ self._request.files.setdefault(name, []).append(dict(
+ filename=name_values["filename"], body=value,
+ content_type=ctype))
+ else:
+ self._request.arguments.setdefault(name, []).append(value)
+
+
+class HTTPRequest(object):
+ """A single HTTP request.
+
+ GET/POST arguments are available in the arguments property, which
+ maps arguments names to lists of values (to support multiple values
+ for individual names). Names and values are both unicode always.
+
+ File uploads are available in the files property, which maps file
+ names to list of files. Each file is a dictionary of the form
+ {"filename":..., "content_type":..., "body":...}. The content_type
+ comes from the provided HTTP header and should not be trusted
+ outright given that it can be easily forged.
+
+ An HTTP request is attached to a single HTTP connection, which can
+ be accessed through the "connection" attribute. Since connections
+ are typically kept open in HTTP/1.1, multiple requests can be handled
+ sequentially on a single connection.
+ """
+ def __init__(self, method, uri, version="HTTP/1.0", headers=None,
+ body=None, remote_ip=None, protocol=None, host=None,
+ files=None, connection=None):
+ self.method = method
+ self.uri = uri
+ self.version = version
+ self.headers = headers or HTTPHeaders()
+ self.body = body or ""
+ if connection and connection.xheaders:
+ # Squid uses X-Forwarded-For, others use X-Real-Ip
+ self.remote_ip = self.headers.get(
+ "X-Real-Ip", self.headers.get("X-Forwarded-For", remote_ip))
+ self.protocol = self.headers.get("X-Scheme", protocol) or "http"
+ else:
+ self.remote_ip = remote_ip
+ self.protocol = protocol or "http"
+ self.host = host or self.headers.get("Host") or "127.0.0.1"
+ self.files = files or {}
+ self.connection = connection
+ self._start_time = time.time()
+ self._finish_time = None
+
+ scheme, netloc, path, query, fragment = urlparse.urlsplit(uri)
+ self.path = path
+ self.query = query
+ arguments = cgi.parse_qs(query)
+ self.arguments = {}
+ for name, values in arguments.iteritems():
+ values = [v for v in values if v]
+ if values: self.arguments[name] = values
+
+ def supports_http_1_1(self):
+ """Returns True if this request supports HTTP/1.1 semantics"""
+ return self.version == "HTTP/1.1"
+
+ def write(self, chunk):
+ """Writes the given chunk to the response stream."""
+ assert isinstance(chunk, str)
+ self.connection.write(chunk)
+
+ def finish(self):
+ """Finishes this HTTP request on the open connection."""
+ self.connection.finish()
+ self._finish_time = time.time()
+
+ def full_url(self):
+ """Reconstructs the full URL for this request."""
+ return self.protocol + "://" + self.host + self.uri
+
+ def request_time(self):
+ """Returns the amount of time it took for this request to execute."""
+ if self._finish_time is None:
+ return time.time() - self._start_time
+ else:
+ return self._finish_time - self._start_time
+
+ def __repr__(self):
+ attrs = ("protocol", "host", "method", "uri", "version", "remote_ip",
+ "remote_ip", "body")
+ args = ", ".join(["%s=%r" % (n, getattr(self, n)) for n in attrs])
+ return "%s(%s, headers=%s)" % (
+ self.__class__.__name__, args, dict(self.headers))
+
+
+class HTTPHeaders(dict):
+ """A dictionary that maintains Http-Header-Case for all keys."""
+ def __setitem__(self, name, value):
+ dict.__setitem__(self, self._normalize_name(name), value)
+
+ def __getitem__(self, name):
+ return dict.__getitem__(self, self._normalize_name(name))
+
+ def _normalize_name(self, name):
+ return "-".join([w.capitalize() for w in name.split("-")])
+
+ @classmethod
+ def parse(cls, headers_string):
+ headers = cls()
+ for line in headers_string.splitlines():
+ if line:
+ name, value = line.split(": ", 1)
+ headers[name] = value
+ return headers
diff --git a/vendor/tornado/tornado/ioloop.py b/vendor/tornado/tornado/ioloop.py
new file mode 100644
index 0000000000..e94c17372e
--- /dev/null
+++ b/vendor/tornado/tornado/ioloop.py
@@ -0,0 +1,483 @@
+#!/usr/bin/env python
+#
+# Copyright 2009 Facebook
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""A level-triggered I/O loop for non-blocking sockets."""
+
+import bisect
+import errno
+import os
+import logging
+import select
+import time
+
+try:
+ import fcntl
+except ImportError:
+ if os.name == 'nt':
+ import win32_support
+ import win32_support as fcntl
+ else:
+ raise
+
+_log = logging.getLogger("tornado.ioloop")
+
+class IOLoop(object):
+ """A level-triggered I/O loop.
+
+ We use epoll if it is available, or else we fall back on select(). If
+ you are implementing a system that needs to handle 1000s of simultaneous
+ connections, you should use Linux and either compile our epoll module or
+ use Python 2.6+ to get epoll support.
+
+ Example usage for a simple TCP server:
+
+ import errno
+ import functools
+ import ioloop
+ import socket
+
+ def connection_ready(sock, fd, events):
+ while True:
+ try:
+ connection, address = sock.accept()
+ except socket.error, e:
+ if e[0] not in (errno.EWOULDBLOCK, errno.EAGAIN):
+ raise
+ return
+ connection.setblocking(0)
+ handle_connection(connection, address)
+
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ sock.setblocking(0)
+ sock.bind(("", port))
+ sock.listen(128)
+
+ io_loop = ioloop.IOLoop.instance()
+ callback = functools.partial(connection_ready, sock)
+ io_loop.add_handler(sock.fileno(), callback, io_loop.READ)
+ io_loop.start()
+
+ """
+ # Constants from the epoll module
+ _EPOLLIN = 0x001
+ _EPOLLPRI = 0x002
+ _EPOLLOUT = 0x004
+ _EPOLLERR = 0x008
+ _EPOLLHUP = 0x010
+ _EPOLLRDHUP = 0x2000
+ _EPOLLONESHOT = (1 << 30)
+ _EPOLLET = (1 << 31)
+
+ # Our events map exactly to the epoll events
+ NONE = 0
+ READ = _EPOLLIN
+ WRITE = _EPOLLOUT
+ ERROR = _EPOLLERR | _EPOLLHUP | _EPOLLRDHUP
+
+ def __init__(self, impl=None):
+ self._impl = impl or _poll()
+ if hasattr(self._impl, 'fileno'):
+ self._set_close_exec(self._impl.fileno())
+ self._handlers = {}
+ self._events = {}
+ self._callbacks = set()
+ self._timeouts = []
+ self._running = False
+ self._stopped = False
+
+ # Create a pipe that we send bogus data to when we want to wake
+ # the I/O loop when it is idle
+ if os.name != 'nt':
+ r, w = os.pipe()
+ self._set_nonblocking(r)
+ self._set_nonblocking(w)
+ self._set_close_exec(r)
+ self._set_close_exec(w)
+ self._waker_reader = os.fdopen(r, "r", 0)
+ self._waker_writer = os.fdopen(w, "w", 0)
+ else:
+ self._waker_reader = self._waker_writer = win32_support.Pipe()
+ r = self._waker_writer.reader_fd
+ self.add_handler(r, self._read_waker, self.READ)
+
+ @classmethod
+ def instance(cls):
+ """Returns a global IOLoop instance.
+
+ Most single-threaded applications have a single, global IOLoop.
+ Use this method instead of passing around IOLoop instances
+ throughout your code.
+
+ A common pattern for classes that depend on IOLoops is to use
+ a default argument to enable programs with multiple IOLoops
+ but not require the argument for simpler applications:
+
+ class MyClass(object):
+ def __init__(self, io_loop=None):
+ self.io_loop = io_loop or IOLoop.instance()
+ """
+ if not hasattr(cls, "_instance"):
+ cls._instance = cls()
+ return cls._instance
+
+ @classmethod
+ def initialized(cls):
+ return hasattr(cls, "_instance")
+
+ def add_handler(self, fd, handler, events):
+ """Registers the given handler to receive the given events for fd."""
+ self._handlers[fd] = handler
+ self._impl.register(fd, events | self.ERROR)
+
+ def update_handler(self, fd, events):
+ """Changes the events we listen for fd."""
+ self._impl.modify(fd, events | self.ERROR)
+
+ def remove_handler(self, fd):
+ """Stop listening for events on fd."""
+ self._handlers.pop(fd, None)
+ self._events.pop(fd, None)
+ try:
+ self._impl.unregister(fd)
+ except (OSError, IOError):
+ _log.debug("Error deleting fd from IOLoop", exc_info=True)
+
+ def start(self):
+ """Starts the I/O loop.
+
+ The loop will run until one of the I/O handlers calls stop(), which
+ will make the loop stop after the current event iteration completes.
+ """
+ if self._stopped:
+ self._stopped = False
+ return
+ self._running = True
+ while True:
+ # Never use an infinite timeout here - it can stall epoll
+ poll_timeout = 0.2
+
+ # Prevent IO event starvation by delaying new callbacks
+ # to the next iteration of the event loop.
+ callbacks = list(self._callbacks)
+ for callback in callbacks:
+ # A callback can add or remove other callbacks
+ if callback in self._callbacks:
+ self._callbacks.remove(callback)
+ self._run_callback(callback)
+
+ if self._callbacks:
+ poll_timeout = 0.0
+
+ if self._timeouts:
+ now = time.time()
+ while self._timeouts and self._timeouts[0].deadline <= now:
+ timeout = self._timeouts.pop(0)
+ self._run_callback(timeout.callback)
+ if self._timeouts:
+ milliseconds = self._timeouts[0].deadline - now
+ poll_timeout = min(milliseconds, poll_timeout)
+
+ if not self._running:
+ break
+
+ try:
+ event_pairs = self._impl.poll(poll_timeout)
+ except Exception, e:
+ if hasattr(e, 'errno') and e.errno == errno.EINTR:
+ _log.warning("Interrupted system call", exc_info=1)
+ continue
+ else:
+ raise
+
+ # Pop one fd at a time from the set of pending fds and run
+ # its handler. Since that handler may perform actions on
+ # other file descriptors, there may be reentrant calls to
+ # this IOLoop that update self._events
+ self._events.update(event_pairs)
+ while self._events:
+ fd, events = self._events.popitem()
+ try:
+ self._handlers[fd](fd, events)
+ except (KeyboardInterrupt, SystemExit):
+ raise
+ except (OSError, IOError), e:
+ if e[0] == errno.EPIPE:
+ # Happens when the client closes the connection
+ pass
+ else:
+ _log.error("Exception in I/O handler for fd %d",
+ fd, exc_info=True)
+ except:
+ _log.error("Exception in I/O handler for fd %d",
+ fd, exc_info=True)
+ # reset the stopped flag so another start/stop pair can be issued
+ self._stopped = False
+
+ def stop(self):
+ """Stop the loop after the current event loop iteration is complete.
+ If the event loop is not currently running, the next call to start()
+ will return immediately.
+
+ To use asynchronous methods from otherwise-synchronous code (such as
+ unit tests), you can start and stop the event loop like this:
+ ioloop = IOLoop()
+ async_method(ioloop=ioloop, callback=ioloop.stop)
+ ioloop.start()
+ ioloop.start() will return after async_method has run its callback,
+ whether that callback was invoked before or after ioloop.start.
+ """
+ self._running = False
+ self._stopped = True
+ self._wake()
+
+ def running(self):
+ """Returns true if this IOLoop is currently running."""
+ return self._running
+
+ def add_timeout(self, deadline, callback):
+ """Calls the given callback at the time deadline from the I/O loop."""
+ timeout = _Timeout(deadline, callback)
+ bisect.insort(self._timeouts, timeout)
+ return timeout
+
+ def remove_timeout(self, timeout):
+ self._timeouts.remove(timeout)
+
+ def add_callback(self, callback):
+ """Calls the given callback on the next I/O loop iteration."""
+ self._callbacks.add(callback)
+ self._wake()
+
+ def remove_callback(self, callback):
+ """Removes the given callback from the next I/O loop iteration."""
+ self._callbacks.remove(callback)
+
+ def _wake(self):
+ try:
+ self._waker_writer.write("x")
+ except IOError:
+ pass
+
+ def _run_callback(self, callback):
+ try:
+ callback()
+ except (KeyboardInterrupt, SystemExit):
+ raise
+ except:
+ self.handle_callback_exception(callback)
+
+ def handle_callback_exception(self, callback):
+ """This method is called whenever a callback run by the IOLoop
+ throws an exception.
+
+ By default simply logs the exception as an error. Subclasses
+ may override this method to customize reporting of exceptions.
+
+ The exception itself is not passed explicitly, but is available
+ in sys.exc_info.
+ """
+ _log.error("Exception in callback %r", callback, exc_info=True)
+
+ def _read_waker(self, fd, events):
+ try:
+ while True:
+ self._waker_reader.read()
+ except IOError:
+ pass
+
+ def _set_nonblocking(self, fd):
+ flags = fcntl.fcntl(fd, fcntl.F_GETFL)
+ fcntl.fcntl(fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)
+
+ def _set_close_exec(self, fd):
+ flags = fcntl.fcntl(fd, fcntl.F_GETFD)
+ fcntl.fcntl(fd, fcntl.F_SETFD, flags | fcntl.FD_CLOEXEC)
+
+
+class _Timeout(object):
+ """An IOLoop timeout, a UNIX timestamp and a callback"""
+
+ # Reduce memory overhead when there are lots of pending callbacks
+ __slots__ = ['deadline', 'callback']
+
+ def __init__(self, deadline, callback):
+ self.deadline = deadline
+ self.callback = callback
+
+ def __cmp__(self, other):
+ return cmp((self.deadline, id(self.callback)),
+ (other.deadline, id(other.callback)))
+
+
+class PeriodicCallback(object):
+ """Schedules the given callback to be called periodically.
+
+ The callback is called every callback_time milliseconds.
+ """
+ def __init__(self, callback, callback_time, io_loop=None):
+ self.callback = callback
+ self.callback_time = callback_time
+ self.io_loop = io_loop or IOLoop.instance()
+ self._running = True
+
+ def start(self):
+ timeout = time.time() + self.callback_time / 1000.0
+ self.io_loop.add_timeout(timeout, self._run)
+
+ def stop(self):
+ self._running = False
+
+ def _run(self):
+ if not self._running: return
+ try:
+ self.callback()
+ except (KeyboardInterrupt, SystemExit):
+ raise
+ except:
+ _log.error("Error in periodic callback", exc_info=True)
+ self.start()
+
+
+class _EPoll(object):
+ """An epoll-based event loop using our C module for Python 2.5 systems"""
+ _EPOLL_CTL_ADD = 1
+ _EPOLL_CTL_DEL = 2
+ _EPOLL_CTL_MOD = 3
+
+ def __init__(self):
+ self._epoll_fd = epoll.epoll_create()
+
+ def fileno(self):
+ return self._epoll_fd
+
+ def register(self, fd, events):
+ epoll.epoll_ctl(self._epoll_fd, self._EPOLL_CTL_ADD, fd, events)
+
+ def modify(self, fd, events):
+ epoll.epoll_ctl(self._epoll_fd, self._EPOLL_CTL_MOD, fd, events)
+
+ def unregister(self, fd):
+ epoll.epoll_ctl(self._epoll_fd, self._EPOLL_CTL_DEL, fd, 0)
+
+ def poll(self, timeout):
+ return epoll.epoll_wait(self._epoll_fd, int(timeout * 1000))
+
+
+class _KQueue(object):
+ """A kqueue-based event loop for BSD/Mac systems."""
+ def __init__(self):
+ self._kqueue = select.kqueue()
+ self._active = {}
+
+ def fileno(self):
+ return self._kqueue.fileno()
+
+ def register(self, fd, events):
+ self._control(fd, events, select.KQ_EV_ADD)
+ self._active[fd] = events
+
+ def modify(self, fd, events):
+ self.unregister(fd)
+ self.register(fd, events)
+
+ def unregister(self, fd):
+ events = self._active.pop(fd)
+ self._control(fd, events, select.KQ_EV_DELETE)
+
+ def _control(self, fd, events, flags):
+ kevents = []
+ if events & IOLoop.WRITE:
+ kevents.append(select.kevent(
+ fd, filter=select.KQ_FILTER_WRITE, flags=flags))
+ if events & IOLoop.READ or not kevents:
+ # Always read when there is not a write
+ kevents.append(select.kevent(
+ fd, filter=select.KQ_FILTER_READ, flags=flags))
+ # Even though control() takes a list, it seems to return EINVAL
+ # on Mac OS X (10.6) when there is more than one event in the list.
+ for kevent in kevents:
+ self._kqueue.control([kevent], 0)
+
+ def poll(self, timeout):
+ kevents = self._kqueue.control(None, 1000, timeout)
+ events = {}
+ for kevent in kevents:
+ fd = kevent.ident
+ flags = 0
+ if kevent.filter == select.KQ_FILTER_READ:
+ events[fd] = events.get(fd, 0) | IOLoop.READ
+ if kevent.filter == select.KQ_FILTER_WRITE:
+ events[fd] = events.get(fd, 0) | IOLoop.WRITE
+ if kevent.flags & select.KQ_EV_ERROR:
+ events[fd] = events.get(fd, 0) | IOLoop.ERROR
+ return events.items()
+
+
+class _Select(object):
+ """A simple, select()-based IOLoop implementation for non-Linux systems"""
+ def __init__(self):
+ self.read_fds = set()
+ self.write_fds = set()
+ self.error_fds = set()
+ self.fd_sets = (self.read_fds, self.write_fds, self.error_fds)
+
+ def register(self, fd, events):
+ if events & IOLoop.READ: self.read_fds.add(fd)
+ if events & IOLoop.WRITE: self.write_fds.add(fd)
+ if events & IOLoop.ERROR: self.error_fds.add(fd)
+
+ def modify(self, fd, events):
+ self.unregister(fd)
+ self.register(fd, events)
+
+ def unregister(self, fd):
+ self.read_fds.discard(fd)
+ self.write_fds.discard(fd)
+ self.error_fds.discard(fd)
+
+ def poll(self, timeout):
+ readable, writeable, errors = select.select(
+ self.read_fds, self.write_fds, self.error_fds, timeout)
+ events = {}
+ for fd in readable:
+ events[fd] = events.get(fd, 0) | IOLoop.READ
+ for fd in writeable:
+ events[fd] = events.get(fd, 0) | IOLoop.WRITE
+ for fd in errors:
+ events[fd] = events.get(fd, 0) | IOLoop.ERROR
+ return events.items()
+
+
+# Choose a poll implementation. Use epoll if it is available, fall back to
+# select() for non-Linux platforms
+if hasattr(select, "epoll"):
+ # Python 2.6+ on Linux
+ _poll = select.epoll
+elif hasattr(select, "kqueue"):
+ # Python 2.6+ on BSD or Mac
+ _poll = _KQueue
+else:
+ try:
+ # Linux systems with our C module installed
+ import epoll
+ _poll = _EPoll
+ except:
+ # All other systems
+ import sys
+ if "linux" in sys.platform:
+ _log.warning("epoll module not found; using select()")
+ _poll = _Select
diff --git a/vendor/tornado/tornado/iostream.py b/vendor/tornado/tornado/iostream.py
new file mode 100644
index 0000000000..af7c6edbfe
--- /dev/null
+++ b/vendor/tornado/tornado/iostream.py
@@ -0,0 +1,229 @@
+#!/usr/bin/env python
+#
+# Copyright 2009 Facebook
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""A utility class to write to and read from a non-blocking socket."""
+
+import errno
+import ioloop
+import logging
+import socket
+
+_log = logging.getLogger('tornado.iostream')
+
+class IOStream(object):
+ """A utility class to write to and read from a non-blocking socket.
+
+ We support three methods: write(), read_until(), and read_bytes().
+ All of the methods take callbacks (since writing and reading are
+ non-blocking and asynchronous). read_until() reads the socket until
+ a given delimiter, and read_bytes() reads until a specified number
+ of bytes have been read from the socket.
+
+ A very simple (and broken) HTTP client using this class:
+
+ import ioloop
+ import iostream
+ import socket
+
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
+ s.connect(("friendfeed.com", 80))
+ stream = IOStream(s)
+
+ def on_headers(data):
+ headers = {}
+ for line in data.split("\r\n"):
+ parts = line.split(":")
+ if len(parts) == 2:
+ headers[parts[0].strip()] = parts[1].strip()
+ stream.read_bytes(int(headers["Content-Length"]), on_body)
+
+ def on_body(data):
+ print data
+ stream.close()
+ ioloop.IOLoop.instance().stop()
+
+ stream.write("GET / HTTP/1.0\r\n\r\n")
+ stream.read_until("\r\n\r\n", on_headers)
+ ioloop.IOLoop.instance().start()
+
+ """
+ def __init__(self, socket, io_loop=None, max_buffer_size=104857600,
+ read_chunk_size=4096):
+ self.socket = socket
+ self.socket.setblocking(False)
+ self.io_loop = io_loop or ioloop.IOLoop.instance()
+ self.max_buffer_size = max_buffer_size
+ self.read_chunk_size = read_chunk_size
+ self._read_buffer = ""
+ self._write_buffer = ""
+ self._read_delimiter = None
+ self._read_bytes = None
+ self._read_callback = None
+ self._write_callback = None
+ self._close_callback = None
+ self._state = self.io_loop.ERROR
+ self.io_loop.add_handler(
+ self.socket.fileno(), self._handle_events, self._state)
+
+ def read_until(self, delimiter, callback):
+ """Call callback when we read the given delimiter."""
+ assert not self._read_callback, "Already reading"
+ loc = self._read_buffer.find(delimiter)
+ if loc != -1:
+ callback(self._consume(loc + len(delimiter)))
+ return
+ self._check_closed()
+ self._read_delimiter = delimiter
+ self._read_callback = callback
+ self._add_io_state(self.io_loop.READ)
+
+ def read_bytes(self, num_bytes, callback):
+ """Call callback when we read the given number of bytes."""
+ assert not self._read_callback, "Already reading"
+ if len(self._read_buffer) >= num_bytes:
+ callback(self._consume(num_bytes))
+ return
+ self._check_closed()
+ self._read_bytes = num_bytes
+ self._read_callback = callback
+ self._add_io_state(self.io_loop.READ)
+
+ def write(self, data, callback=None):
+ """Write the given data to this stream.
+
+ If callback is given, we call it when all of the buffered write
+ data has been successfully written to the stream. If there was
+ previously buffered write data and an old write callback, that
+ callback is simply overwritten with this new callback.
+ """
+ self._check_closed()
+ self._write_buffer += data
+ self._add_io_state(self.io_loop.WRITE)
+ self._write_callback = callback
+
+ def set_close_callback(self, callback):
+ """Call the given callback when the stream is closed."""
+ self._close_callback = callback
+
+ def close(self):
+ """Close this stream."""
+ if self.socket is not None:
+ self.io_loop.remove_handler(self.socket.fileno())
+ self.socket.close()
+ self.socket = None
+ if self._close_callback: self._close_callback()
+
+ def reading(self):
+ """Returns true if we are currently reading from the stream."""
+ return self._read_callback is not None
+
+ def writing(self):
+ """Returns true if we are currently writing to the stream."""
+ return len(self._write_buffer) > 0
+
+ def closed(self):
+ return self.socket is None
+
+ def _handle_events(self, fd, events):
+ if not self.socket:
+ _log.warning("Got events for closed stream %d", fd)
+ return
+ if events & self.io_loop.READ:
+ self._handle_read()
+ if not self.socket:
+ return
+ if events & self.io_loop.WRITE:
+ self._handle_write()
+ if not self.socket:
+ return
+ if events & self.io_loop.ERROR:
+ self.close()
+ return
+ state = self.io_loop.ERROR
+ if self._read_delimiter or self._read_bytes:
+ state |= self.io_loop.READ
+ if self._write_buffer:
+ state |= self.io_loop.WRITE
+ if state != self._state:
+ self._state = state
+ self.io_loop.update_handler(self.socket.fileno(), self._state)
+
+ def _handle_read(self):
+ try:
+ chunk = self.socket.recv(self.read_chunk_size)
+ except socket.error, e:
+ if e[0] in (errno.EWOULDBLOCK, errno.EAGAIN):
+ return
+ else:
+ _log.warning("Read error on %d: %s",
+ self.socket.fileno(), e)
+ self.close()
+ return
+ if not chunk:
+ self.close()
+ return
+ self._read_buffer += chunk
+ if len(self._read_buffer) >= self.max_buffer_size:
+ _log.error("Reached maximum read buffer size")
+ self.close()
+ return
+ if self._read_bytes:
+ if len(self._read_buffer) >= self._read_bytes:
+ num_bytes = self._read_bytes
+ callback = self._read_callback
+ self._read_callback = None
+ self._read_bytes = None
+ callback(self._consume(num_bytes))
+ elif self._read_delimiter:
+ loc = self._read_buffer.find(self._read_delimiter)
+ if loc != -1:
+ callback = self._read_callback
+ delimiter_len = len(self._read_delimiter)
+ self._read_callback = None
+ self._read_delimiter = None
+ callback(self._consume(loc + delimiter_len))
+
+ def _handle_write(self):
+ while self._write_buffer:
+ try:
+ num_bytes = self.socket.send(self._write_buffer)
+ self._write_buffer = self._write_buffer[num_bytes:]
+ except socket.error, e:
+ if e[0] in (errno.EWOULDBLOCK, errno.EAGAIN):
+ break
+ else:
+ _log.warning("Write error on %d: %s",
+ self.socket.fileno(), e)
+ self.close()
+ return
+ if not self._write_buffer and self._write_callback:
+ callback = self._write_callback
+ self._write_callback = None
+ callback()
+
+ def _consume(self, loc):
+ result = self._read_buffer[:loc]
+ self._read_buffer = self._read_buffer[loc:]
+ return result
+
+ def _check_closed(self):
+ if not self.socket:
+ raise IOError("Stream is closed")
+
+ def _add_io_state(self, state):
+ if not self._state & state:
+ self._state = self._state | state
+ self.io_loop.update_handler(self.socket.fileno(), self._state)
diff --git a/vendor/tornado/tornado/locale.py b/vendor/tornado/tornado/locale.py
new file mode 100644
index 0000000000..6a8537d750
--- /dev/null
+++ b/vendor/tornado/tornado/locale.py
@@ -0,0 +1,457 @@
+#!/usr/bin/env python
+#
+# Copyright 2009 Facebook
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""Translation methods for generating localized strings.
+
+To load a locale and generate a translated string:
+
+ user_locale = locale.get("es_LA")
+ print user_locale.translate("Sign out")
+
+locale.get() returns the closest matching locale, not necessarily the
+specific locale you requested. You can support pluralization with
+additional arguments to translate(), e.g.:
+
+ people = [...]
+ message = user_locale.translate(
+ "%(list)s is online", "%(list)s are online", len(people))
+ print message % {"list": user_locale.list(people)}
+
+The first string is chosen if len(people) == 1, otherwise the second
+string is chosen.
+
+Applications should call one of load_translations (which uses a simple
+CSV format) or load_gettext_translations (which uses the .mo format
+supported by gettext and related tools). If neither method is called,
+the locale.translate method will simply return the original string.
+"""
+
+import csv
+import datetime
+import logging
+import os
+import os.path
+import re
+
+_default_locale = "en_US"
+_translations = {}
+_supported_locales = frozenset([_default_locale])
+_use_gettext = False
+
+_log = logging.getLogger('tornado.locale')
+
+def get(*locale_codes):
+ """Returns the closest match for the given locale codes.
+
+ We iterate over all given locale codes in order. If we have a tight
+ or a loose match for the code (e.g., "en" for "en_US"), we return
+ the locale. Otherwise we move to the next code in the list.
+
+ By default we return en_US if no translations are found for any of
+ the specified locales. You can change the default locale with
+ set_default_locale() below.
+ """
+ return Locale.get_closest(*locale_codes)
+
+
+def set_default_locale(code):
+ """Sets the default locale, used in get_closest_locale().
+
+ The default locale is assumed to be the language used for all strings
+ in the system. The translations loaded from disk are mappings from
+ the default locale to the destination locale. Consequently, you don't
+ need to create a translation file for the default locale.
+ """
+ global _default_locale
+ global _supported_locales
+ _default_locale = code
+ _supported_locales = frozenset(_translations.keys() + [_default_locale])
+
+
+def load_translations(directory):
+ """Loads translations from CSV files in a directory.
+
+ Translations are strings with optional Python-style named placeholders
+ (e.g., "My name is %(name)s") and their associated translations.
+
+ The directory should have translation files of the form LOCALE.csv,
+ e.g. es_GT.csv. The CSV files should have two or three columns: string,
+ translation, and an optional plural indicator. Plural indicators should
+ be one of "plural" or "singular". A given string can have both singular
+ and plural forms. For example "%(name)s liked this" may have a
+ different verb conjugation depending on whether %(name)s is one
+ name or a list of names. There should be two rows in the CSV file for
+ that string, one with plural indicator "singular", and one "plural".
+ For strings with no verbs that would change on translation, simply
+ use "unknown" or the empty string (or don't include the column at all).
+
+ Example translation es_LA.csv:
+
+ "I love you","Te amo"
+ "%(name)s liked this","A %(name)s les gust\xf3 esto","plural"
+ "%(name)s liked this","A %(name)s le gust\xf3 esto","singular"
+
+ """
+ global _translations
+ global _supported_locales
+ _translations = {}
+ for path in os.listdir(directory):
+ if not path.endswith(".csv"): continue
+ locale, extension = path.split(".")
+ if locale not in LOCALE_NAMES:
+ _log.error("Unrecognized locale %r (path: %s)", locale,
+ os.path.join(directory, path))
+ continue
+ f = open(os.path.join(directory, path), "r")
+ _translations[locale] = {}
+ for i, row in enumerate(csv.reader(f)):
+ if not row or len(row) < 2: continue
+ row = [c.decode("utf-8").strip() for c in row]
+ english, translation = row[:2]
+ if len(row) > 2:
+ plural = row[2] or "unknown"
+ else:
+ plural = "unknown"
+ if plural not in ("plural", "singular", "unknown"):
+ _log.error("Unrecognized plural indicator %r in %s line %d",
+ plural, path, i + 1)
+ continue
+ _translations[locale].setdefault(plural, {})[english] = translation
+ f.close()
+ _supported_locales = frozenset(_translations.keys() + [_default_locale])
+ _log.info("Supported locales: %s", sorted(_supported_locales))
+
+def load_gettext_translations(directory, domain):
+ """Loads translations from gettext's locale tree
+
+ Locale tree is similar to system's /usr/share/locale, like:
+
+ {directory}/{lang}/LC_MESSAGES/{domain}.mo
+
+ Three steps are required to have you app translated:
+
+ 1. Generate POT translation file
+ xgettext --language=Python --keyword=_:1,2 -d cyclone file1.py file2.html etc
+
+ 2. Merge against existing POT file:
+ msgmerge old.po cyclone.po > new.po
+
+ 3. Compile:
+ msgfmt cyclone.po -o {directory}/pt_BR/LC_MESSAGES/cyclone.mo
+ """
+ import gettext
+ global _translations
+ global _supported_locales
+ global _use_gettext
+ _translations = {}
+ for lang in os.listdir(directory):
+ if os.path.isfile(os.path.join(directory, lang)): continue
+ try:
+ os.stat(os.path.join(directory, lang, "LC_MESSAGES", domain+".mo"))
+ _translations[lang] = gettext.translation(domain, directory,
+ languages=[lang])
+ except Exception, e:
+ logging.error("Cannot load translation for '%s': %s", lang, str(e))
+ continue
+ _supported_locales = frozenset(_translations.keys() + [_default_locale])
+ _use_gettext = True
+ _log.info("Supported locales: %s", sorted(_supported_locales))
+
+
+def get_supported_locales(cls):
+ """Returns a list of all the supported locale codes."""
+ return _supported_locales
+
+
+class Locale(object):
+ @classmethod
+ def get_closest(cls, *locale_codes):
+ """Returns the closest match for the given locale code."""
+ for code in locale_codes:
+ if not code: continue
+ code = code.replace("-", "_")
+ parts = code.split("_")
+ if len(parts) > 2:
+ continue
+ elif len(parts) == 2:
+ code = parts[0].lower() + "_" + parts[1].upper()
+ if code in _supported_locales:
+ return cls.get(code)
+ if parts[0].lower() in _supported_locales:
+ return cls.get(parts[0].lower())
+ return cls.get(_default_locale)
+
+ @classmethod
+ def get(cls, code):
+ """Returns the Locale for the given locale code.
+
+ If it is not supported, we raise an exception.
+ """
+ if not hasattr(cls, "_cache"):
+ cls._cache = {}
+ if code not in cls._cache:
+ assert code in _supported_locales
+ translations = _translations.get(code, None)
+ if translations is None:
+ locale = CSVLocale(code, {})
+ elif _use_gettext:
+ locale = GettextLocale(code, translations)
+ else:
+ locale = CSVLocale(code, translations)
+ cls._cache[code] = locale
+ return cls._cache[code]
+
+ def __init__(self, code, translations):
+ self.code = code
+ self.name = LOCALE_NAMES.get(code, {}).get("name", u"Unknown")
+ self.rtl = False
+ for prefix in ["fa", "ar", "he"]:
+ if self.code.startswith(prefix):
+ self.rtl = True
+ break
+ self.translations = translations
+
+ # Initialize strings for date formatting
+ _ = self.translate
+ self._months = [
+ _("January"), _("February"), _("March"), _("April"),
+ _("May"), _("June"), _("July"), _("August"),
+ _("September"), _("October"), _("November"), _("December")]
+ self._weekdays = [
+ _("Monday"), _("Tuesday"), _("Wednesday"), _("Thursday"),
+ _("Friday"), _("Saturday"), _("Sunday")]
+
+ def translate(self, message, plural_message=None, count=None):
+ raise NotImplementedError()
+
+ def format_date(self, date, gmt_offset=0, relative=True, shorter=False,
+ full_format=False):
+ """Formats the given date (which should be GMT).
+
+ By default, we return a relative time (e.g., "2 minutes ago"). You
+ can return an absolute date string with relative=False.
+
+ You can force a full format date ("July 10, 1980") with
+ full_format=True.
+ """
+ if self.code.startswith("ru"):
+ relative = False
+ if type(date) in (int, long, float):
+ date = datetime.datetime.utcfromtimestamp(date)
+ now = datetime.datetime.utcnow()
+ # Round down to now. Due to click skew, things are somethings
+ # slightly in the future.
+ if date > now: date = now
+ local_date = date - datetime.timedelta(minutes=gmt_offset)
+ local_now = now - datetime.timedelta(minutes=gmt_offset)
+ local_yesterday = local_now - datetime.timedelta(hours=24)
+ difference = now - date
+ seconds = difference.seconds
+ days = difference.days
+
+ _ = self.translate
+ format = None
+ if not full_format:
+ if relative and days == 0:
+ if seconds < 50:
+ return _("1 second ago", "%(seconds)d seconds ago",
+ seconds) % { "seconds": seconds }
+
+ if seconds < 50 * 60:
+ minutes = round(seconds / 60.0)
+ return _("1 minute ago", "%(minutes)d minutes ago",
+ minutes) % { "minutes": minutes }
+
+ hours = round(seconds / (60.0 * 60))
+ return _("1 hour ago", "%(hours)d hours ago",
+ hours) % { "hours": hours }
+
+ if days == 0:
+ format = _("%(time)s")
+ elif days == 1 and local_date.day == local_yesterday.day and \
+ relative:
+ format = _("yesterday") if shorter else \
+ _("yesterday at %(time)s")
+ elif days < 5:
+ format = _("%(weekday)s") if shorter else \
+ _("%(weekday)s at %(time)s")
+ elif days < 334: # 11mo, since confusing for same month last year
+ format = _("%(month_name)s %(day)s") if shorter else \
+ _("%(month_name)s %(day)s at %(time)s")
+
+ if format is None:
+ format = _("%(month_name)s %(day)s, %(year)s") if shorter else \
+ _("%(month_name)s %(day)s, %(year)s at %(time)s")
+
+ tfhour_clock = self.code not in ("en", "en_US", "zh_CN")
+ if tfhour_clock:
+ str_time = "%d:%02d" % (local_date.hour, local_date.minute)
+ elif self.code == "zh_CN":
+ str_time = "%s%d:%02d" % (
+ (u'\u4e0a\u5348', u'\u4e0b\u5348')[local_date.hour >= 12],
+ local_date.hour % 12 or 12, local_date.minute)
+ else:
+ str_time = "%d:%02d %s" % (
+ local_date.hour % 12 or 12, local_date.minute,
+ ("am", "pm")[local_date.hour >= 12])
+
+ return format % {
+ "month_name": self._months[local_date.month - 1],
+ "weekday": self._weekdays[local_date.weekday()],
+ "day": str(local_date.day),
+ "year": str(local_date.year),
+ "time": str_time
+ }
+
+ def format_day(self, date, gmt_offset=0, dow=True):
+ """Formats the given date as a day of week.
+
+ Example: "Monday, January 22". You can remove the day of week with
+ dow=False.
+ """
+ local_date = date - datetime.timedelta(minutes=gmt_offset)
+ _ = self.translate
+ if dow:
+ return _("%(weekday)s, %(month_name)s %(day)s") % {
+ "month_name": self._months[local_date.month - 1],
+ "weekday": self._weekdays[local_date.weekday()],
+ "day": str(local_date.day),
+ }
+ else:
+ return _("%(month_name)s %(day)s") % {
+ "month_name": self._months[local_date.month - 1],
+ "day": str(local_date.day),
+ }
+
+ def list(self, parts):
+ """Returns a comma-separated list for the given list of parts.
+
+ The format is, e.g., "A, B and C", "A and B" or just "A" for lists
+ of size 1.
+ """
+ _ = self.translate
+ if len(parts) == 0: return ""
+ if len(parts) == 1: return parts[0]
+ comma = u' \u0648 ' if self.code.startswith("fa") else u", "
+ return _("%(commas)s and %(last)s") % {
+ "commas": comma.join(parts[:-1]),
+ "last": parts[len(parts) - 1],
+ }
+
+ def friendly_number(self, value):
+ """Returns a comma-separated number for the given integer."""
+ if self.code not in ("en", "en_US"):
+ return str(value)
+ value = str(value)
+ parts = []
+ while value:
+ parts.append(value[-3:])
+ value = value[:-3]
+ return ",".join(reversed(parts))
+
+class CSVLocale(Locale):
+ """Locale implementation using tornado's CSV translation format."""
+ def translate(self, message, plural_message=None, count=None):
+ """Returns the translation for the given message for this locale.
+
+ If plural_message is given, you must also provide count. We return
+ plural_message when count != 1, and we return the singular form
+ for the given message when count == 1.
+ """
+ if plural_message is not None:
+ assert count is not None
+ if count != 1:
+ message = plural_message
+ message_dict = self.translations.get("plural", {})
+ else:
+ message_dict = self.translations.get("singular", {})
+ else:
+ message_dict = self.translations.get("unknown", {})
+ return message_dict.get(message, message)
+
+class GettextLocale(Locale):
+ """Locale implementation using the gettext module."""
+ def translate(self, message, plural_message=None, count=None):
+ if plural_message is not None:
+ assert count is not None
+ return self.translations.ungettext(message, plural_message, count)
+ else:
+ return self.translations.ugettext(message)
+
+LOCALE_NAMES = {
+ "af_ZA": {"name_en": u"Afrikaans", "name": u"Afrikaans"},
+ "ar_AR": {"name_en": u"Arabic", "name": u"\u0627\u0644\u0639\u0631\u0628\u064a\u0629"},
+ "bg_BG": {"name_en": u"Bulgarian", "name": u"\u0411\u044a\u043b\u0433\u0430\u0440\u0441\u043a\u0438"},
+ "bn_IN": {"name_en": u"Bengali", "name": u"\u09ac\u09be\u0982\u09b2\u09be"},
+ "bs_BA": {"name_en": u"Bosnian", "name": u"Bosanski"},
+ "ca_ES": {"name_en": u"Catalan", "name": u"Catal\xe0"},
+ "cs_CZ": {"name_en": u"Czech", "name": u"\u010ce\u0161tina"},
+ "cy_GB": {"name_en": u"Welsh", "name": u"Cymraeg"},
+ "da_DK": {"name_en": u"Danish", "name": u"Dansk"},
+ "de_DE": {"name_en": u"German", "name": u"Deutsch"},
+ "el_GR": {"name_en": u"Greek", "name": u"\u0395\u03bb\u03bb\u03b7\u03bd\u03b9\u03ba\u03ac"},
+ "en_GB": {"name_en": u"English (UK)", "name": u"English (UK)"},
+ "en_US": {"name_en": u"English (US)", "name": u"English (US)"},
+ "es_ES": {"name_en": u"Spanish (Spain)", "name": u"Espa\xf1ol (Espa\xf1a)"},
+ "es_LA": {"name_en": u"Spanish", "name": u"Espa\xf1ol"},
+ "et_EE": {"name_en": u"Estonian", "name": u"Eesti"},
+ "eu_ES": {"name_en": u"Basque", "name": u"Euskara"},
+ "fa_IR": {"name_en": u"Persian", "name": u"\u0641\u0627\u0631\u0633\u06cc"},
+ "fi_FI": {"name_en": u"Finnish", "name": u"Suomi"},
+ "fr_CA": {"name_en": u"French (Canada)", "name": u"Fran\xe7ais (Canada)"},
+ "fr_FR": {"name_en": u"French", "name": u"Fran\xe7ais"},
+ "ga_IE": {"name_en": u"Irish", "name": u"Gaeilge"},
+ "gl_ES": {"name_en": u"Galician", "name": u"Galego"},
+ "he_IL": {"name_en": u"Hebrew", "name": u"\u05e2\u05d1\u05e8\u05d9\u05ea"},
+ "hi_IN": {"name_en": u"Hindi", "name": u"\u0939\u093f\u0928\u094d\u0926\u0940"},
+ "hr_HR": {"name_en": u"Croatian", "name": u"Hrvatski"},
+ "hu_HU": {"name_en": u"Hungarian", "name": u"Magyar"},
+ "id_ID": {"name_en": u"Indonesian", "name": u"Bahasa Indonesia"},
+ "is_IS": {"name_en": u"Icelandic", "name": u"\xcdslenska"},
+ "it_IT": {"name_en": u"Italian", "name": u"Italiano"},
+ "ja_JP": {"name_en": u"Japanese", "name": u"\xe6\xe6\xe8"},
+ "ko_KR": {"name_en": u"Korean", "name": u"\xed\xea\xec"},
+ "lt_LT": {"name_en": u"Lithuanian", "name": u"Lietuvi\u0173"},
+ "lv_LV": {"name_en": u"Latvian", "name": u"Latvie\u0161u"},
+ "mk_MK": {"name_en": u"Macedonian", "name": u"\u041c\u0430\u043a\u0435\u0434\u043e\u043d\u0441\u043a\u0438"},
+ "ml_IN": {"name_en": u"Malayalam", "name": u"\u0d2e\u0d32\u0d2f\u0d3e\u0d33\u0d02"},
+ "ms_MY": {"name_en": u"Malay", "name": u"Bahasa Melayu"},
+ "nb_NO": {"name_en": u"Norwegian (bokmal)", "name": u"Norsk (bokm\xe5l)"},
+ "nl_NL": {"name_en": u"Dutch", "name": u"Nederlands"},
+ "nn_NO": {"name_en": u"Norwegian (nynorsk)", "name": u"Norsk (nynorsk)"},
+ "pa_IN": {"name_en": u"Punjabi", "name": u"\u0a2a\u0a70\u0a1c\u0a3e\u0a2c\u0a40"},
+ "pl_PL": {"name_en": u"Polish", "name": u"Polski"},
+ "pt_BR": {"name_en": u"Portuguese (Brazil)", "name": u"Portugu\xeas (Brasil)"},
+ "pt_PT": {"name_en": u"Portuguese (Portugal)", "name": u"Portugu\xeas (Portugal)"},
+ "ro_RO": {"name_en": u"Romanian", "name": u"Rom\xe2n\u0103"},
+ "ru_RU": {"name_en": u"Russian", "name": u"\u0420\u0443\u0441\u0441\u043a\u0438\u0439"},
+ "sk_SK": {"name_en": u"Slovak", "name": u"Sloven\u010dina"},
+ "sl_SI": {"name_en": u"Slovenian", "name": u"Sloven\u0161\u010dina"},
+ "sq_AL": {"name_en": u"Albanian", "name": u"Shqip"},
+ "sr_RS": {"name_en": u"Serbian", "name": u"\u0421\u0440\u043f\u0441\u043a\u0438"},
+ "sv_SE": {"name_en": u"Swedish", "name": u"Svenska"},
+ "sw_KE": {"name_en": u"Swahili", "name": u"Kiswahili"},
+ "ta_IN": {"name_en": u"Tamil", "name": u"\u0ba4\u0bae\u0bbf\u0bb4\u0bcd"},
+ "te_IN": {"name_en": u"Telugu", "name": u"\u0c24\u0c46\u0c32\u0c41\u0c17\u0c41"},
+ "th_TH": {"name_en": u"Thai", "name": u"\u0e20\u0e32\u0e29\u0e32\u0e44\u0e17\u0e22"},
+ "tl_PH": {"name_en": u"Filipino", "name": u"Filipino"},
+ "tr_TR": {"name_en": u"Turkish", "name": u"T\xfcrk\xe7e"},
+ "uk_UA": {"name_en": u"Ukraini ", "name": u"\u0423\u043a\u0440\u0430\u0457\u043d\u0441\u044c\u043a\u0430"},
+ "vi_VN": {"name_en": u"Vietnamese", "name": u"Ti\u1ebfng Vi\u1ec7t"},
+ "zh_CN": {"name_en": u"Chinese (Simplified)", "name": u"\xe4\xe6(\xe7\xe4)"},
+ "zh_HK": {"name_en": u"Chinese (Hong Kong)", "name": u"\xe4\xe6(\xe9\xe6)"},
+ "zh_TW": {"name_en": u"Chinese (Taiwan)", "name": u"\xe4\xe6(\xe5\xe7)"},
+}
diff --git a/vendor/tornado/tornado/options.py b/vendor/tornado/tornado/options.py
new file mode 100644
index 0000000000..66bce091e7
--- /dev/null
+++ b/vendor/tornado/tornado/options.py
@@ -0,0 +1,386 @@
+#!/usr/bin/env python
+#
+# Copyright 2009 Facebook
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""A command line parsing module that lets modules define their own options.
+
+Each module defines its own options, e.g.,
+
+ from tornado.options import define, options
+
+ define("mysql_host", default="127.0.0.1:3306", help="Main user DB")
+ define("memcache_hosts", default="127.0.0.1:11011", multiple=True,
+ help="Main user memcache servers")
+
+ def connect():
+ db = database.Connection(options.mysql_host)
+ ...
+
+The main() method of your application does not need to be aware of all of
+the options used throughout your program; they are all automatically loaded
+when the modules are loaded. Your main() method can parse the command line
+or parse a config file with:
+
+ import tornado.options
+ tornado.options.parse_config_file("/etc/server.conf")
+ tornado.options.parse_command_line()
+
+Command line formats are what you would expect ("--myoption=myvalue").
+Config files are just Python files. Global names become options, e.g.,
+
+ myoption = "myvalue"
+ myotheroption = "myothervalue"
+
+We support datetimes, timedeltas, ints, and floats (just pass a 'type'
+kwarg to define). We also accept multi-value options. See the documentation
+for define() below.
+"""
+
+import datetime
+import logging
+import logging.handlers
+import re
+import sys
+import time
+
+# For pretty log messages, if available
+try:
+ import curses
+except:
+ curses = None
+
+
+def define(name, default=None, type=str, help=None, metavar=None,
+ multiple=False):
+ """Defines a new command line option.
+
+ If type is given (one of str, float, int, datetime, or timedelta),
+ we parse the command line arguments based on the given type. If
+ multiple is True, we accept comma-separated values, and the option
+ value is always a list.
+
+ For multi-value integers, we also accept the syntax x:y, which
+ turns into range(x, y) - very useful for long integer ranges.
+
+ help and metavar are used to construct the automatically generated
+ command line help string. The help message is formatted like:
+
+ --name=METAVAR help string
+
+ Command line option names must be unique globally. They can be parsed
+ from the command line with parse_command_line() or parsed from a
+ config file with parse_config_file.
+ """
+ if name in options:
+ raise Error("Option %r already defined in %s", name,
+ options[name].file_name)
+ frame = sys._getframe(0)
+ options_file = frame.f_code.co_filename
+ file_name = frame.f_back.f_code.co_filename
+ if file_name == options_file: file_name = ""
+ options[name] = _Option(name, file_name=file_name, default=default,
+ type=type, help=help, metavar=metavar,
+ multiple=multiple)
+
+
+def parse_command_line(args=None):
+ """Parses all options given on the command line.
+
+ We return all command line arguments that are not options as a list.
+ """
+ if args is None: args = sys.argv
+ remaining = []
+ for i in xrange(1, len(args)):
+ # All things after the last option are command line arguments
+ if not args[i].startswith("-"):
+ remaining = args[i:]
+ break
+ if args[i] == "--":
+ remaining = args[i+1:]
+ break
+ arg = args[i].lstrip("-")
+ name, equals, value = arg.partition("=")
+ name = name.replace('-', '_')
+ if not name in options:
+ print_help()
+ raise Error('Unrecognized command line option: %r' % name)
+ option = options[name]
+ if not equals:
+ if option.type == bool:
+ value = "true"
+ else:
+ raise Error('Option %r requires a value' % name)
+ option.parse(value)
+ if options.help:
+ print_help()
+ sys.exit(0)
+
+ # Set up log level and pretty console logging by default
+ if options.logging != 'none':
+ logging.getLogger().setLevel(getattr(logging, options.logging.upper()))
+ enable_pretty_logging()
+
+ return remaining
+
+
+def parse_config_file(path, overwrite=True):
+ """Parses and loads the Python config file at the given path."""
+ config = {}
+ execfile(path, config, config)
+ for name in config:
+ if name in options:
+ options[name].set(config[name])
+
+
+def print_help(file=sys.stdout):
+ """Prints all the command line options to stdout."""
+ print >> file, "Usage: %s [OPTIONS]" % sys.argv[0]
+ print >> file, ""
+ print >> file, "Options:"
+ by_file = {}
+ for option in options.itervalues():
+ by_file.setdefault(option.file_name, []).append(option)
+
+ for filename, o in sorted(by_file.items()):
+ if filename: print >> file, filename
+ o.sort(key=lambda option: option.name)
+ for option in o:
+ prefix = option.name
+ if option.metavar:
+ prefix += "=" + option.metavar
+ print >> file, " --%-30s %s" % (prefix, option.help or "")
+ print >> file
+
+
+class _Options(dict):
+ """Our global program options, an dictionary with object-like access."""
+ @classmethod
+ def instance(cls):
+ if not hasattr(cls, "_instance"):
+ cls._instance = cls()
+ return cls._instance
+
+ def __getattr__(self, name):
+ if isinstance(self.get(name), _Option):
+ return self[name].value()
+ raise AttributeError("Unrecognized option %r" % name)
+
+
+class _Option(object):
+ def __init__(self, name, default=None, type=str, help=None, metavar=None,
+ multiple=False, file_name=None):
+ if default is None and multiple:
+ default = []
+ self.name = name
+ self.type = type
+ self.help = help
+ self.metavar = metavar
+ self.multiple = multiple
+ self.file_name = file_name
+ self.default = default
+ self._value = None
+
+ def value(self):
+ return self.default if self._value is None else self._value
+
+ def parse(self, value):
+ _parse = {
+ datetime.datetime: self._parse_datetime,
+ datetime.timedelta: self._parse_timedelta,
+ bool: self._parse_bool,
+ str: self._parse_string,
+ }.get(self.type, self.type)
+ if self.multiple:
+ if self._value is None:
+ self._value = []
+ for part in value.split(","):
+ if self.type in (int, long):
+ # allow ranges of the form X:Y (inclusive at both ends)
+ lo, _, hi = part.partition(":")
+ lo = _parse(lo)
+ hi = _parse(hi) if hi else lo
+ self._value.extend(range(lo, hi+1))
+ else:
+ self._value.append(_parse(part))
+ else:
+ self._value = _parse(value)
+ return self.value()
+
+ def set(self, value):
+ if self.multiple:
+ if not isinstance(value, list):
+ raise Error("Option %r is required to be a list of %s" %
+ (self.name, self.type.__name__))
+ for item in value:
+ if item != None and not isinstance(item, self.type):
+ raise Error("Option %r is required to be a list of %s" %
+ (self.name, self.type.__name__))
+ else:
+ if value != None and not isinstance(value, self.type):
+ raise Error("Option %r is required to be a %s" %
+ (self.name, self.type.__name__))
+ self._value = value
+
+ # Supported date/time formats in our options
+ _DATETIME_FORMATS = [
+ "%a %b %d %H:%M:%S %Y",
+ "%Y-%m-%d %H:%M:%S",
+ "%Y-%m-%d %H:%M",
+ "%Y-%m-%dT%H:%M",
+ "%Y%m%d %H:%M:%S",
+ "%Y%m%d %H:%M",
+ "%Y-%m-%d",
+ "%Y%m%d",
+ "%H:%M:%S",
+ "%H:%M",
+ ]
+
+ def _parse_datetime(self, value):
+ for format in self._DATETIME_FORMATS:
+ try:
+ return datetime.datetime.strptime(value, format)
+ except ValueError:
+ pass
+ raise Error('Unrecognized date/time format: %r' % value)
+
+ _TIMEDELTA_ABBREVS = [
+ ('hours', ['h']),
+ ('minutes', ['m', 'min']),
+ ('seconds', ['s', 'sec']),
+ ('milliseconds', ['ms']),
+ ('microseconds', ['us']),
+ ('days', ['d']),
+ ('weeks', ['w']),
+ ]
+
+ _TIMEDELTA_ABBREV_DICT = dict(
+ (abbrev, full) for full, abbrevs in _TIMEDELTA_ABBREVS
+ for abbrev in abbrevs)
+
+ _FLOAT_PATTERN = r'[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?'
+
+ _TIMEDELTA_PATTERN = re.compile(
+ r'\s*(%s)\s*(\w*)\s*' % _FLOAT_PATTERN, re.IGNORECASE)
+
+ def _parse_timedelta(self, value):
+ try:
+ sum = datetime.timedelta()
+ start = 0
+ while start < len(value):
+ m = self._TIMEDELTA_PATTERN.match(value, start)
+ if not m:
+ raise Exception()
+ num = float(m.group(1))
+ units = m.group(2) or 'seconds'
+ units = self._TIMEDELTA_ABBREV_DICT.get(units, units)
+ sum += datetime.timedelta(**{units: num})
+ start = m.end()
+ return sum
+ except:
+ raise
+
+ def _parse_bool(self, value):
+ return value.lower() not in ("false", "0", "f")
+
+ def _parse_string(self, value):
+ return value.decode("utf-8")
+
+
+class Error(Exception):
+ pass
+
+
+def enable_pretty_logging():
+ """Turns on formatted logging output as configured."""
+ if (options.log_to_stderr or
+ (options.log_to_stderr is None and not options.log_file_prefix)):
+ # Set up color if we are in a tty and curses is installed
+ color = False
+ if curses and sys.stderr.isatty():
+ try:
+ curses.setupterm()
+ if curses.tigetnum("colors") > 0:
+ color = True
+ except:
+ pass
+ channel = logging.StreamHandler()
+ channel.setFormatter(_LogFormatter(color=color))
+ logging.getLogger().addHandler(channel)
+
+ if options.log_file_prefix:
+ channel = logging.handlers.RotatingFileHandler(
+ filename=options.log_file_prefix,
+ maxBytes=options.log_file_max_size,
+ backupCount=options.log_file_num_backups)
+ channel.setFormatter(_LogFormatter(color=False))
+ logging.getLogger().addHandler(channel)
+
+
+class _LogFormatter(logging.Formatter):
+ def __init__(self, color, *args, **kwargs):
+ logging.Formatter.__init__(self, *args, **kwargs)
+ self._color = color
+ if color:
+ fg_color = curses.tigetstr("setaf") or curses.tigetstr("setf") or ""
+ self._colors = {
+ logging.DEBUG: curses.tparm(fg_color, 4), # Blue
+ logging.INFO: curses.tparm(fg_color, 2), # Green
+ logging.WARNING: curses.tparm(fg_color, 3), # Yellow
+ logging.ERROR: curses.tparm(fg_color, 1), # Red
+ }
+ self._normal = curses.tigetstr("sgr0")
+
+ def format(self, record):
+ try:
+ record.message = record.getMessage()
+ except Exception, e:
+ record.message = "Bad message (%r): %r" % (e, record.__dict__)
+ record.asctime = time.strftime(
+ "%y%m%d %H:%M:%S", self.converter(record.created))
+ prefix = '[%(levelname)1.1s %(asctime)s %(module)s:%(lineno)d]' % \
+ record.__dict__
+ if self._color:
+ prefix = (self._colors.get(record.levelno, self._normal) +
+ prefix + self._normal)
+ formatted = prefix + " " + record.message
+ if record.exc_info:
+ if not record.exc_text:
+ record.exc_text = self.formatException(record.exc_info)
+ if record.exc_text:
+ formatted = formatted.rstrip() + "\n" + record.exc_text
+ return formatted.replace("\n", "\n ")
+
+
+options = _Options.instance()
+
+
+# Default options
+define("help", type=bool, help="show this help information")
+define("logging", default="info",
+ help=("Set the Python log level. If 'none', tornado won't touch the "
+ "logging configuration."),
+ metavar="info|warning|error|none")
+define("log_to_stderr", type=bool, default=None,
+ help=("Send log output to stderr (colorized if possible). "
+ "By default use stderr if --log_file_prefix is not set."))
+define("log_file_prefix", type=str, default=None, metavar="PATH",
+ help=("Path prefix for log files. "
+ "Note that if you are running multiple tornado processes, "
+ "log_file_prefix must be different for each of them (e.g. "
+ "include the port number)"))
+define("log_file_max_size", type=int, default=100 * 1000 * 1000,
+ help="max size of log files before rollover")
+define("log_file_num_backups", type=int, default=10,
+ help="number of log files to keep")
diff --git a/vendor/tornado/tornado/s3server.py b/vendor/tornado/tornado/s3server.py
new file mode 100644
index 0000000000..2e8a97de20
--- /dev/null
+++ b/vendor/tornado/tornado/s3server.py
@@ -0,0 +1,255 @@
+#!/usr/bin/env python
+#
+# Copyright 2009 Facebook
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""Implementation of an S3-like storage server based on local files.
+
+Useful to test features that will eventually run on S3, or if you want to
+run something locally that was once running on S3.
+
+We don't support all the features of S3, but it does work with the
+standard S3 client for the most basic semantics. To use the standard
+S3 client with this module:
+
+ c = S3.AWSAuthConnection("", "", server="localhost", port=8888,
+ is_secure=False)
+ c.create_bucket("mybucket")
+ c.put("mybucket", "mykey", "a value")
+ print c.get("mybucket", "mykey").body
+
+"""
+
+import bisect
+import datetime
+import escape
+import hashlib
+import httpserver
+import ioloop
+import os
+import os.path
+import urllib
+import web
+
+
+def start(port, root_directory="/tmp/s3", bucket_depth=0):
+ """Starts the mock S3 server on the given port at the given path."""
+ application = S3Application(root_directory, bucket_depth)
+ http_server = httpserver.HTTPServer(application)
+ http_server.listen(port)
+ ioloop.IOLoop.instance().start()
+
+
+class S3Application(web.Application):
+ """Implementation of an S3-like storage server based on local files.
+
+ If bucket depth is given, we break files up into multiple directories
+ to prevent hitting file system limits for number of files in each
+ directories. 1 means one level of directories, 2 means 2, etc.
+ """
+ def __init__(self, root_directory, bucket_depth=0):
+ web.Application.__init__(self, [
+ (r"/", RootHandler),
+ (r"/([^/]+)/(.+)", ObjectHandler),
+ (r"/([^/]+)/", BucketHandler),
+ ])
+ self.directory = os.path.abspath(root_directory)
+ if not os.path.exists(self.directory):
+ os.makedirs(self.directory)
+ self.bucket_depth = bucket_depth
+
+
+class BaseRequestHandler(web.RequestHandler):
+ SUPPORTED_METHODS = ("PUT", "GET", "DELETE")
+
+ def render_xml(self, value):
+ assert isinstance(value, dict) and len(value) == 1
+ self.set_header("Content-Type", "application/xml; charset=UTF-8")
+ name = value.keys()[0]
+ parts = []
+ parts.append('<' + escape.utf8(name) +
+ ' xmlns="http://doc.s3.amazonaws.com/2006-03-01">')
+ self._render_parts(value.values()[0], parts)
+ parts.append('</' + escape.utf8(name) + '>')
+ self.finish('<?xml version="1.0" encoding="UTF-8"?>\n' +
+ ''.join(parts))
+
+ def _render_parts(self, value, parts=[]):
+ if isinstance(value, basestring):
+ parts.append(escape.xhtml_escape(value))
+ elif isinstance(value, int) or isinstance(value, long):
+ parts.append(str(value))
+ elif isinstance(value, datetime.datetime):
+ parts.append(value.strftime("%Y-%m-%dT%H:%M:%S.000Z"))
+ elif isinstance(value, dict):
+ for name, subvalue in value.iteritems():
+ if not isinstance(subvalue, list):
+ subvalue = [subvalue]
+ for subsubvalue in subvalue:
+ parts.append('<' + escape.utf8(name) + '>')
+ self._render_parts(subsubvalue, parts)
+ parts.append('</' + escape.utf8(name) + '>')
+ else:
+ raise Exception("Unknown S3 value type %r", value)
+
+ def _object_path(self, bucket, object_name):
+ if self.application.bucket_depth < 1:
+ return os.path.abspath(os.path.join(
+ self.application.directory, bucket, object_name))
+ hash = hashlib.md5(object_name).hexdigest()
+ path = os.path.abspath(os.path.join(
+ self.application.directory, bucket))
+ for i in range(self.application.bucket_depth):
+ path = os.path.join(path, hash[:2 * (i + 1)])
+ return os.path.join(path, object_name)
+
+
+class RootHandler(BaseRequestHandler):
+ def get(self):
+ names = os.listdir(self.application.directory)
+ buckets = []
+ for name in names:
+ path = os.path.join(self.application.directory, name)
+ info = os.stat(path)
+ buckets.append({
+ "Name": name,
+ "CreationDate": datetime.datetime.utcfromtimestamp(
+ info.st_ctime),
+ })
+ self.render_xml({"ListAllMyBucketsResult": {
+ "Buckets": {"Bucket": buckets},
+ }})
+
+
+class BucketHandler(BaseRequestHandler):
+ def get(self, bucket_name):
+ prefix = self.get_argument("prefix", u"")
+ marker = self.get_argument("marker", u"")
+ max_keys = int(self.get_argument("max-keys", 50000))
+ path = os.path.abspath(os.path.join(self.application.directory,
+ bucket_name))
+ terse = int(self.get_argument("terse", 0))
+ if not path.startswith(self.application.directory) or \
+ not os.path.isdir(path):
+ raise web.HTTPError(404)
+ object_names = []
+ for root, dirs, files in os.walk(path):
+ for file_name in files:
+ object_names.append(os.path.join(root, file_name))
+ skip = len(path) + 1
+ for i in range(self.application.bucket_depth):
+ skip += 2 * (i + 1) + 1
+ object_names = [n[skip:] for n in object_names]
+ object_names.sort()
+ contents = []
+
+ start_pos = 0
+ if marker:
+ start_pos = bisect.bisect_right(object_names, marker, start_pos)
+ if prefix:
+ start_pos = bisect.bisect_left(object_names, prefix, start_pos)
+
+ truncated = False
+ for object_name in object_names[start_pos:]:
+ if not object_name.startswith(prefix):
+ break
+ if len(contents) >= max_keys:
+ truncated = True
+ break
+ object_path = self._object_path(bucket_name, object_name)
+ c = {"Key": object_name}
+ if not terse:
+ info = os.stat(object_path)
+ c.update({
+ "LastModified": datetime.datetime.utcfromtimestamp(
+ info.st_mtime),
+ "Size": info.st_size,
+ })
+ contents.append(c)
+ marker = object_name
+ self.render_xml({"ListBucketResult": {
+ "Name": bucket_name,
+ "Prefix": prefix,
+ "Marker": marker,
+ "MaxKeys": max_keys,
+ "IsTruncated": truncated,
+ "Contents": contents,
+ }})
+
+ def put(self, bucket_name):
+ path = os.path.abspath(os.path.join(
+ self.application.directory, bucket_name))
+ if not path.startswith(self.application.directory) or \
+ os.path.exists(path):
+ raise web.HTTPError(403)
+ os.makedirs(path)
+ self.finish()
+
+ def delete(self, bucket_name):
+ path = os.path.abspath(os.path.join(
+ self.application.directory, bucket_name))
+ if not path.startswith(self.application.directory) or \
+ not os.path.isdir(path):
+ raise web.HTTPError(404)
+ if len(os.listdir(path)) > 0:
+ raise web.HTTPError(403)
+ os.rmdir(path)
+ self.set_status(204)
+ self.finish()
+
+
+class ObjectHandler(BaseRequestHandler):
+ def get(self, bucket, object_name):
+ object_name = urllib.unquote(object_name)
+ path = self._object_path(bucket, object_name)
+ if not path.startswith(self.application.directory) or \
+ not os.path.isfile(path):
+ raise web.HTTPError(404)
+ info = os.stat(path)
+ self.set_header("Content-Type", "application/unknown")
+ self.set_header("Last-Modified", datetime.datetime.utcfromtimestamp(
+ info.st_mtime))
+ object_file = open(path, "r")
+ try:
+ self.finish(object_file.read())
+ finally:
+ object_file.close()
+
+ def put(self, bucket, object_name):
+ object_name = urllib.unquote(object_name)
+ bucket_dir = os.path.abspath(os.path.join(
+ self.application.directory, bucket))
+ if not bucket_dir.startswith(self.application.directory) or \
+ not os.path.isdir(bucket_dir):
+ raise web.HTTPError(404)
+ path = self._object_path(bucket, object_name)
+ if not path.startswith(bucket_dir) or os.path.isdir(path):
+ raise web.HTTPError(403)
+ directory = os.path.dirname(path)
+ if not os.path.exists(directory):
+ os.makedirs(directory)
+ object_file = open(path, "w")
+ object_file.write(self.request.body)
+ object_file.close()
+ self.finish()
+
+ def delete(self, bucket, object_name):
+ object_name = urllib.unquote(object_name)
+ path = self._object_path(bucket, object_name)
+ if not path.startswith(self.application.directory) or \
+ not os.path.isfile(path):
+ raise web.HTTPError(404)
+ os.unlink(path)
+ self.set_status(204)
+ self.finish()
diff --git a/vendor/tornado/tornado/template.py b/vendor/tornado/tornado/template.py
new file mode 100644
index 0000000000..7ed56cfa69
--- /dev/null
+++ b/vendor/tornado/tornado/template.py
@@ -0,0 +1,576 @@
+#!/usr/bin/env python
+#
+# Copyright 2009 Facebook
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""A simple template system that compiles templates to Python code.
+
+Basic usage looks like:
+
+ t = template.Template("<html>{{ myvalue }}</html>")
+ print t.generate(myvalue="XXX")
+
+Loader is a class that loads templates from a root directory and caches
+the compiled templates:
+
+ loader = template.Loader("/home/btaylor")
+ print loader.load("test.html").generate(myvalue="XXX")
+
+We compile all templates to raw Python. Error-reporting is currently... uh,
+interesting. Syntax for the templates
+
+ ### base.html
+ <html>
+ <head>
+ <title>{% block title %}Default title{% end %}</title>
+ </head>
+ <body>
+ <ul>
+ {% for student in students %}
+ {% block student %}
+ <li>{{ escape(student.name) }}</li>
+ {% end %}
+ {% end %}
+ </ul>
+ </body>
+ </html>
+
+ ### bold.html
+ {% extends "base.html" %}
+
+ {% block title %}A bolder title{% end %}
+
+ {% block student %}
+ <li><span style="bold">{{ escape(student.name) }}</span></li>
+ {% block %}
+
+Unlike most other template systems, we do not put any restrictions on the
+expressions you can include in your statements. if and for blocks get
+translated exactly into Python, do you can do complex expressions like:
+
+ {% for student in [p for p in people if p.student and p.age > 23] %}
+ <li>{{ escape(student.name) }}</li>
+ {% end %}
+
+Translating directly to Python means you can apply functions to expressions
+easily, like the escape() function in the examples above. You can pass
+functions in to your template just like any other variable:
+
+ ### Python code
+ def add(x, y):
+ return x + y
+ template.execute(add=add)
+
+ ### The template
+ {{ add(1, 2) }}
+
+We provide the functions escape(), url_escape(), json_encode(), and squeeze()
+to all templates by default.
+"""
+
+from __future__ import with_statement
+
+import cStringIO
+import datetime
+import escape
+import logging
+import os.path
+import re
+
+_log = logging.getLogger('tornado.template')
+
+class Template(object):
+ """A compiled template.
+
+ We compile into Python from the given template_string. You can generate
+ the template from variables with generate().
+ """
+ def __init__(self, template_string, name="<string>", loader=None,
+ compress_whitespace=None):
+ self.name = name
+ if compress_whitespace is None:
+ compress_whitespace = name.endswith(".html") or \
+ name.endswith(".js")
+ reader = _TemplateReader(name, template_string)
+ self.file = _File(_parse(reader))
+ self.code = self._generate_python(loader, compress_whitespace)
+ try:
+ self.compiled = compile(self.code, self.name, "exec")
+ except:
+ formatted_code = _format_code(self.code).rstrip()
+ _log.error("%s code:\n%s", self.name, formatted_code)
+ raise
+
+ def generate(self, **kwargs):
+ """Generate this template with the given arguments."""
+ namespace = {
+ "escape": escape.xhtml_escape,
+ "url_escape": escape.url_escape,
+ "json_encode": escape.json_encode,
+ "squeeze": escape.squeeze,
+ "datetime": datetime,
+ }
+ namespace.update(kwargs)
+ exec self.compiled in namespace
+ execute = namespace["_execute"]
+ try:
+ return execute()
+ except:
+ formatted_code = _format_code(self.code).rstrip()
+ _log.error("%s code:\n%s", self.name, formatted_code)
+ raise
+
+ def _generate_python(self, loader, compress_whitespace):
+ buffer = cStringIO.StringIO()
+ try:
+ named_blocks = {}
+ ancestors = self._get_ancestors(loader)
+ ancestors.reverse()
+ for ancestor in ancestors:
+ ancestor.find_named_blocks(loader, named_blocks)
+ self.file.find_named_blocks(loader, named_blocks)
+ writer = _CodeWriter(buffer, named_blocks, loader, self,
+ compress_whitespace)
+ ancestors[0].generate(writer)
+ return buffer.getvalue()
+ finally:
+ buffer.close()
+
+ def _get_ancestors(self, loader):
+ ancestors = [self.file]
+ for chunk in self.file.body.chunks:
+ if isinstance(chunk, _ExtendsBlock):
+ if not loader:
+ raise ParseError("{% extends %} block found, but no "
+ "template loader")
+ template = loader.load(chunk.name, self.name)
+ ancestors.extend(template._get_ancestors(loader))
+ return ancestors
+
+
+class Loader(object):
+ """A template loader that loads from a single root directory.
+
+ You must use a template loader to use template constructs like
+ {% extends %} and {% include %}. Loader caches all templates after
+ they are loaded the first time.
+ """
+ def __init__(self, root_directory):
+ self.root = os.path.abspath(root_directory)
+ self.templates = {}
+
+ def reset(self):
+ self.templates = {}
+
+ def resolve_path(self, name, parent_path=None):
+ if parent_path and not parent_path.startswith("<") and \
+ not parent_path.startswith("/") and \
+ not name.startswith("/"):
+ current_path = os.path.join(self.root, parent_path)
+ file_dir = os.path.dirname(os.path.abspath(current_path))
+ relative_path = os.path.abspath(os.path.join(file_dir, name))
+ if relative_path.startswith(self.root):
+ name = relative_path[len(self.root) + 1:]
+ return name
+
+ def load(self, name, parent_path=None):
+ name = self.resolve_path(name, parent_path=parent_path)
+ if name not in self.templates:
+ path = os.path.join(self.root, name)
+ f = open(path, "r")
+ self.templates[name] = Template(f.read(), name=name, loader=self)
+ f.close()
+ return self.templates[name]
+
+
+class _Node(object):
+ def each_child(self):
+ return ()
+
+ def generate(self, writer):
+ raise NotImplementedError()
+
+ def find_named_blocks(self, loader, named_blocks):
+ for child in self.each_child():
+ child.find_named_blocks(loader, named_blocks)
+
+
+class _File(_Node):
+ def __init__(self, body):
+ self.body = body
+
+ def generate(self, writer):
+ writer.write_line("def _execute():")
+ with writer.indent():
+ writer.write_line("_buffer = []")
+ self.body.generate(writer)
+ writer.write_line("return ''.join(_buffer)")
+
+ def each_child(self):
+ return (self.body,)
+
+
+
+class _ChunkList(_Node):
+ def __init__(self, chunks):
+ self.chunks = chunks
+
+ def generate(self, writer):
+ for chunk in self.chunks:
+ chunk.generate(writer)
+
+ def each_child(self):
+ return self.chunks
+
+
+class _NamedBlock(_Node):
+ def __init__(self, name, body=None):
+ self.name = name
+ self.body = body
+
+ def each_child(self):
+ return (self.body,)
+
+ def generate(self, writer):
+ writer.named_blocks[self.name].generate(writer)
+
+ def find_named_blocks(self, loader, named_blocks):
+ named_blocks[self.name] = self.body
+ _Node.find_named_blocks(self, loader, named_blocks)
+
+
+class _ExtendsBlock(_Node):
+ def __init__(self, name):
+ self.name = name
+
+
+class _IncludeBlock(_Node):
+ def __init__(self, name, reader):
+ self.name = name
+ self.template_name = reader.name
+
+ def find_named_blocks(self, loader, named_blocks):
+ included = loader.load(self.name, self.template_name)
+ included.file.find_named_blocks(loader, named_blocks)
+
+ def generate(self, writer):
+ included = writer.loader.load(self.name, self.template_name)
+ old = writer.current_template
+ writer.current_template = included
+ included.file.body.generate(writer)
+ writer.current_template = old
+
+
+class _ApplyBlock(_Node):
+ def __init__(self, method, body=None):
+ self.method = method
+ self.body = body
+
+ def each_child(self):
+ return (self.body,)
+
+ def generate(self, writer):
+ method_name = "apply%d" % writer.apply_counter
+ writer.apply_counter += 1
+ writer.write_line("def %s():" % method_name)
+ with writer.indent():
+ writer.write_line("_buffer = []")
+ self.body.generate(writer)
+ writer.write_line("return ''.join(_buffer)")
+ writer.write_line("_buffer.append(%s(%s()))" % (
+ self.method, method_name))
+
+
+class _ControlBlock(_Node):
+ def __init__(self, statement, body=None):
+ self.statement = statement
+ self.body = body
+
+ def each_child(self):
+ return (self.body,)
+
+ def generate(self, writer):
+ writer.write_line("%s:" % self.statement)
+ with writer.indent():
+ self.body.generate(writer)
+
+
+class _IntermediateControlBlock(_Node):
+ def __init__(self, statement):
+ self.statement = statement
+
+ def generate(self, writer):
+ writer.write_line("%s:" % self.statement, writer.indent_size() - 1)
+
+
+class _Statement(_Node):
+ def __init__(self, statement):
+ self.statement = statement
+
+ def generate(self, writer):
+ writer.write_line(self.statement)
+
+
+class _Expression(_Node):
+ def __init__(self, expression):
+ self.expression = expression
+
+ def generate(self, writer):
+ writer.write_line("_tmp = %s" % self.expression)
+ writer.write_line("if isinstance(_tmp, str): _buffer.append(_tmp)")
+ writer.write_line("elif isinstance(_tmp, unicode): "
+ "_buffer.append(_tmp.encode('utf-8'))")
+ writer.write_line("else: _buffer.append(str(_tmp))")
+
+
+class _Text(_Node):
+ def __init__(self, value):
+ self.value = value
+
+ def generate(self, writer):
+ value = self.value
+
+ # Compress lots of white space to a single character. If the whitespace
+ # breaks a line, have it continue to break a line, but just with a
+ # single \n character
+ if writer.compress_whitespace and "<pre>" not in value:
+ value = re.sub(r"([\t ]+)", " ", value)
+ value = re.sub(r"(\s*\n\s*)", "\n", value)
+
+ if value:
+ writer.write_line('_buffer.append(%r)' % value)
+
+
+class ParseError(Exception):
+ """Raised for template syntax errors."""
+ pass
+
+
+class _CodeWriter(object):
+ def __init__(self, file, named_blocks, loader, current_template,
+ compress_whitespace):
+ self.file = file
+ self.named_blocks = named_blocks
+ self.loader = loader
+ self.current_template = current_template
+ self.compress_whitespace = compress_whitespace
+ self.apply_counter = 0
+ self._indent = 0
+
+ def indent(self):
+ return self
+
+ def indent_size(self):
+ return self._indent
+
+ def __enter__(self):
+ self._indent += 1
+ return self
+
+ def __exit__(self, *args):
+ assert self._indent > 0
+ self._indent -= 1
+
+ def write_line(self, line, indent=None):
+ if indent == None:
+ indent = self._indent
+ for i in xrange(indent):
+ self.file.write(" ")
+ print >> self.file, line
+
+
+class _TemplateReader(object):
+ def __init__(self, name, text):
+ self.name = name
+ self.text = text
+ self.line = 0
+ self.pos = 0
+
+ def find(self, needle, start=0, end=None):
+ assert start >= 0, start
+ pos = self.pos
+ start += pos
+ if end is None:
+ index = self.text.find(needle, start)
+ else:
+ end += pos
+ assert end >= start
+ index = self.text.find(needle, start, end)
+ if index != -1:
+ index -= pos
+ return index
+
+ def consume(self, count=None):
+ if count is None:
+ count = len(self.text) - self.pos
+ newpos = self.pos + count
+ self.line += self.text.count("\n", self.pos, newpos)
+ s = self.text[self.pos:newpos]
+ self.pos = newpos
+ return s
+
+ def remaining(self):
+ return len(self.text) - self.pos
+
+ def __len__(self):
+ return self.remaining()
+
+ def __getitem__(self, key):
+ if type(key) is slice:
+ size = len(self)
+ start, stop, step = slice.indices(size)
+ if start is None: start = self.pos
+ else: start += self.pos
+ if stop is not None: stop += self.pos
+ return self.text[slice(start, stop, step)]
+ elif key < 0:
+ return self.text[key]
+ else:
+ return self.text[self.pos + key]
+
+ def __str__(self):
+ return self.text[self.pos:]
+
+
+def _format_code(code):
+ lines = code.splitlines()
+ format = "%%%dd %%s\n" % len(repr(len(lines) + 1))
+ return "".join([format % (i + 1, line) for (i, line) in enumerate(lines)])
+
+
+def _parse(reader, in_block=None):
+ body = _ChunkList([])
+ while True:
+ # Find next template directive
+ curly = 0
+ while True:
+ curly = reader.find("{", curly)
+ if curly == -1 or curly + 1 == reader.remaining():
+ # EOF
+ if in_block:
+ raise ParseError("Missing {%% end %%} block for %s" %
+ in_block)
+ body.chunks.append(_Text(reader.consume()))
+ return body
+ # If the first curly brace is not the start of a special token,
+ # start searching from the character after it
+ if reader[curly + 1] not in ("{", "%"):
+ curly += 1
+ continue
+ # When there are more than 2 curlies in a row, use the
+ # innermost ones. This is useful when generating languages
+ # like latex where curlies are also meaningful
+ if (curly + 2 < reader.remaining() and
+ reader[curly + 1] == '{' and reader[curly + 2] == '{'):
+ curly += 1
+ continue
+ break
+
+ # Append any text before the special token
+ if curly > 0:
+ body.chunks.append(_Text(reader.consume(curly)))
+
+ start_brace = reader.consume(2)
+ line = reader.line
+
+ # Expression
+ if start_brace == "{{":
+ end = reader.find("}}")
+ if end == -1 or reader.find("\n", 0, end) != -1:
+ raise ParseError("Missing end expression }} on line %d" % line)
+ contents = reader.consume(end).strip()
+ reader.consume(2)
+ if not contents:
+ raise ParseError("Empty expression on line %d" % line)
+ body.chunks.append(_Expression(contents))
+ continue
+
+ # Block
+ assert start_brace == "{%", start_brace
+ end = reader.find("%}")
+ if end == -1 or reader.find("\n", 0, end) != -1:
+ raise ParseError("Missing end block %%} on line %d" % line)
+ contents = reader.consume(end).strip()
+ reader.consume(2)
+ if not contents:
+ raise ParseError("Empty block tag ({%% %%}) on line %d" % line)
+
+ operator, space, suffix = contents.partition(" ")
+ suffix = suffix.strip()
+
+ # Intermediate ("else", "elif", etc) blocks
+ intermediate_blocks = {
+ "else": set(["if", "for", "while"]),
+ "elif": set(["if"]),
+ "except": set(["try"]),
+ "finally": set(["try"]),
+ }
+ allowed_parents = intermediate_blocks.get(operator)
+ if allowed_parents is not None:
+ if not in_block:
+ raise ParseError("%s outside %s block" %
+ (operator, allowed_parents))
+ if in_block not in allowed_parents:
+ raise ParseError("%s block cannot be attached to %s block" % (operator, in_block))
+ body.chunks.append(_IntermediateControlBlock(contents))
+ continue
+
+ # End tag
+ elif operator == "end":
+ if not in_block:
+ raise ParseError("Extra {%% end %%} block on line %d" % line)
+ return body
+
+ elif operator in ("extends", "include", "set", "import", "comment"):
+ if operator == "comment":
+ continue
+ if operator == "extends":
+ suffix = suffix.strip('"').strip("'")
+ if not suffix:
+ raise ParseError("extends missing file path on line %d" % line)
+ block = _ExtendsBlock(suffix)
+ elif operator == "import":
+ if not suffix:
+ raise ParseError("import missing statement on line %d" % line)
+ block = _Statement(contents)
+ elif operator == "include":
+ suffix = suffix.strip('"').strip("'")
+ if not suffix:
+ raise ParseError("include missing file path on line %d" % line)
+ block = _IncludeBlock(suffix, reader)
+ elif operator == "set":
+ if not suffix:
+ raise ParseError("set missing statement on line %d" % line)
+ block = _Statement(suffix)
+ body.chunks.append(block)
+ continue
+
+ elif operator in ("apply", "block", "try", "if", "for", "while"):
+ # parse inner body recursively
+ block_body = _parse(reader, operator)
+ if operator == "apply":
+ if not suffix:
+ raise ParseError("apply missing method name on line %d" % line)
+ block = _ApplyBlock(suffix, block_body)
+ elif operator == "block":
+ if not suffix:
+ raise ParseError("block missing name on line %d" % line)
+ block = _NamedBlock(suffix, block_body)
+ else:
+ block = _ControlBlock(contents, block_body)
+ body.chunks.append(block)
+ continue
+
+ else:
+ raise ParseError("unknown operator: %r" % operator)
diff --git a/vendor/tornado/tornado/test/README b/vendor/tornado/tornado/test/README
new file mode 100644
index 0000000000..2d6195d807
--- /dev/null
+++ b/vendor/tornado/tornado/test/README
@@ -0,0 +1,4 @@
+Test coverage is almost non-existent, but it's a start. Be sure to
+set PYTHONPATH apprioriately (generally to the root directory of your
+tornado checkout) when running tests to make sure you're getting the
+version of the tornado package that you expect. \ No newline at end of file
diff --git a/vendor/tornado/tornado/test/test_ioloop.py b/vendor/tornado/tornado/test/test_ioloop.py
new file mode 100755
index 0000000000..2541fa87e1
--- /dev/null
+++ b/vendor/tornado/tornado/test/test_ioloop.py
@@ -0,0 +1,38 @@
+#!/usr/bin/env python
+
+import unittest
+import time
+
+from tornado import ioloop
+
+
+class TestIOLoop(unittest.TestCase):
+ def setUp(self):
+ self.loop = ioloop.IOLoop()
+
+ def tearDown(self):
+ pass
+
+ def _callback(self):
+ self.called = True
+ self.loop.stop()
+
+ def _schedule_callback(self):
+ self.loop.add_callback(self._callback)
+ # Scroll away the time so we can check if we woke up immediately
+ self._start_time = time.time()
+ self.called = False
+
+ def test_add_callback(self):
+ self.loop.add_timeout(time.time(), self._schedule_callback)
+ self.loop.start() # Set some long poll timeout so we can check wakeup
+ self.assertAlmostEqual(time.time(), self._start_time, places=2)
+ self.assertTrue(self.called)
+
+
+if __name__ == "__main__":
+ import logging
+
+ logging.basicConfig(level=logging.DEBUG, format='%(asctime)s:%(msecs)03d %(levelname)-8s %(name)-8s %(message)s', datefmt='%H:%M:%S')
+
+ unittest.main()
diff --git a/vendor/tornado/tornado/web.py b/vendor/tornado/tornado/web.py
new file mode 100644
index 0000000000..7559fae8a5
--- /dev/null
+++ b/vendor/tornado/tornado/web.py
@@ -0,0 +1,1445 @@
+#!/usr/bin/env python
+#
+# Copyright 2009 Facebook
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""The Tornado web framework.
+
+The Tornado web framework looks a bit like web.py (http://webpy.org/) or
+Google's webapp (http://code.google.com/appengine/docs/python/tools/webapp/),
+but with additional tools and optimizations to take advantage of the
+Tornado non-blocking web server and tools.
+
+Here is the canonical "Hello, world" example app:
+
+ import tornado.httpserver
+ import tornado.ioloop
+ import tornado.web
+
+ class MainHandler(tornado.web.RequestHandler):
+ def get(self):
+ self.write("Hello, world")
+
+ if __name__ == "__main__":
+ application = tornado.web.Application([
+ (r"/", MainHandler),
+ ])
+ http_server = tornado.httpserver.HTTPServer(application)
+ http_server.listen(8888)
+ tornado.ioloop.IOLoop.instance().start()
+
+See the Tornado walkthrough on GitHub for more details and a good
+getting started guide.
+"""
+
+import base64
+import binascii
+import calendar
+import Cookie
+import cStringIO
+import datetime
+import email.utils
+import escape
+import functools
+import gzip
+import hashlib
+import hmac
+import httplib
+import locale
+import logging
+import mimetypes
+import os.path
+import re
+import stat
+import sys
+import template
+import time
+import types
+import urllib
+import urlparse
+import uuid
+
+_log = logging.getLogger('tornado.web')
+
+class RequestHandler(object):
+ """Subclass this class and define get() or post() to make a handler.
+
+ If you want to support more methods than the standard GET/HEAD/POST, you
+ should override the class variable SUPPORTED_METHODS in your
+ RequestHandler class.
+ """
+ SUPPORTED_METHODS = ("GET", "HEAD", "POST", "DELETE", "PUT")
+
+ def __init__(self, application, request, transforms=None):
+ self.application = application
+ self.request = request
+ self._headers_written = False
+ self._finished = False
+ self._auto_finish = True
+ self._transforms = transforms or []
+ self.ui = _O((n, self._ui_method(m)) for n, m in
+ application.ui_methods.iteritems())
+ self.ui["modules"] = _O((n, self._ui_module(n, m)) for n, m in
+ application.ui_modules.iteritems())
+ self.clear()
+ # Check since connection is not available in WSGI
+ if hasattr(self.request, "connection"):
+ self.request.connection.stream.set_close_callback(
+ self.on_connection_close)
+
+ @property
+ def settings(self):
+ return self.application.settings
+
+ def head(self, *args, **kwargs):
+ raise HTTPError(405)
+
+ def get(self, *args, **kwargs):
+ raise HTTPError(405)
+
+ def post(self, *args, **kwargs):
+ raise HTTPError(405)
+
+ def delete(self, *args, **kwargs):
+ raise HTTPError(405)
+
+ def put(self, *args, **kwargs):
+ raise HTTPError(405)
+
+ def prepare(self):
+ """Called before the actual handler method.
+
+ Useful to override in a handler if you want a common bottleneck for
+ all of your requests.
+ """
+ pass
+
+ def on_connection_close(self):
+ """Called in async handlers if the client closed the connection.
+
+ You may override this to clean up resources associated with
+ long-lived connections.
+
+ Note that the select()-based implementation of IOLoop does not detect
+ closed connections and so this method will not be called until
+ you try (and fail) to produce some output. The epoll- and kqueue-
+ based implementations should detect closed connections even while
+ the request is idle.
+ """
+ pass
+
+ def clear(self):
+ """Resets all headers and content for this response."""
+ self._headers = {
+ "Server": "TornadoServer/0.1",
+ "Content-Type": "text/html; charset=UTF-8",
+ }
+ if not self.request.supports_http_1_1():
+ if self.request.headers.get("Connection") == "Keep-Alive":
+ self.set_header("Connection", "Keep-Alive")
+ self._write_buffer = []
+ self._status_code = 200
+
+ def set_status(self, status_code):
+ """Sets the status code for our response."""
+ assert status_code in httplib.responses
+ self._status_code = status_code
+
+ def set_header(self, name, value):
+ """Sets the given response header name and value.
+
+ If a datetime is given, we automatically format it according to the
+ HTTP specification. If the value is not a string, we convert it to
+ a string. All header values are then encoded as UTF-8.
+ """
+ if isinstance(value, datetime.datetime):
+ t = calendar.timegm(value.utctimetuple())
+ value = email.utils.formatdate(t, localtime=False, usegmt=True)
+ elif isinstance(value, int) or isinstance(value, long):
+ value = str(value)
+ else:
+ value = _utf8(value)
+ # If \n is allowed into the header, it is possible to inject
+ # additional headers or split the request. Also cap length to
+ # prevent obviously erroneous values.
+ safe_value = re.sub(r"[\x00-\x1f]", " ", value)[:4000]
+ if safe_value != value:
+ raise ValueError("Unsafe header value %r", value)
+ self._headers[name] = value
+
+ _ARG_DEFAULT = []
+ def get_argument(self, name, default=_ARG_DEFAULT, strip=True):
+ """Returns the value of the argument with the given name.
+
+ If default is not provided, the argument is considered to be
+ required, and we throw an HTTP 404 exception if it is missing.
+
+ The returned value is always unicode.
+ """
+ values = self.request.arguments.get(name, None)
+ if values is None:
+ if default is self._ARG_DEFAULT:
+ raise HTTPError(404, "Missing argument %s" % name)
+ return default
+ # Get rid of any weird control chars
+ value = re.sub(r"[\x00-\x08\x0e-\x1f]", " ", values[-1])
+ value = _unicode(value)
+ if strip: value = value.strip()
+ return value
+
+ @property
+ def cookies(self):
+ """A dictionary of Cookie.Morsel objects."""
+ if not hasattr(self, "_cookies"):
+ self._cookies = Cookie.BaseCookie()
+ if "Cookie" in self.request.headers:
+ try:
+ self._cookies.load(self.request.headers["Cookie"])
+ except:
+ self.clear_all_cookies()
+ return self._cookies
+
+ def get_cookie(self, name, default=None):
+ """Gets the value of the cookie with the given name, else default."""
+ if name in self.cookies:
+ return self.cookies[name].value
+ return default
+
+ def set_cookie(self, name, value, domain=None, expires=None, path="/",
+ expires_days=None):
+ """Sets the given cookie name/value with the given options."""
+ name = _utf8(name)
+ value = _utf8(value)
+ if re.search(r"[\x00-\x20]", name + value):
+ # Don't let us accidentally inject bad stuff
+ raise ValueError("Invalid cookie %r: %r" % (name, value))
+ if not hasattr(self, "_new_cookies"):
+ self._new_cookies = []
+ new_cookie = Cookie.BaseCookie()
+ self._new_cookies.append(new_cookie)
+ new_cookie[name] = value
+ if domain:
+ new_cookie[name]["domain"] = domain
+ if expires_days is not None and not expires:
+ expires = datetime.datetime.utcnow() + datetime.timedelta(
+ days=expires_days)
+ if expires:
+ timestamp = calendar.timegm(expires.utctimetuple())
+ new_cookie[name]["expires"] = email.utils.formatdate(
+ timestamp, localtime=False, usegmt=True)
+ if path:
+ new_cookie[name]["path"] = path
+
+ def clear_cookie(self, name, path="/", domain=None):
+ """Deletes the cookie with the given name."""
+ expires = datetime.datetime.utcnow() - datetime.timedelta(days=365)
+ self.set_cookie(name, value="", path=path, expires=expires,
+ domain=domain)
+
+ def clear_all_cookies(self):
+ """Deletes all the cookies the user sent with this request."""
+ for name in self.cookies.iterkeys():
+ self.clear_cookie(name)
+
+ def set_secure_cookie(self, name, value, expires_days=30, **kwargs):
+ """Signs and timestamps a cookie so it cannot be forged.
+
+ You must specify the 'cookie_secret' setting in your Application
+ to use this method. It should be a long, random sequence of bytes
+ to be used as the HMAC secret for the signature.
+
+ To read a cookie set with this method, use get_secure_cookie().
+ """
+ timestamp = str(int(time.time()))
+ value = base64.b64encode(value)
+ signature = self._cookie_signature(name, value, timestamp)
+ value = "|".join([value, timestamp, signature])
+ self.set_cookie(name, value, expires_days=expires_days, **kwargs)
+
+ def get_secure_cookie(self, name, include_name=True, value=None):
+ """Returns the given signed cookie if it validates, or None.
+
+ In older versions of Tornado (0.1 and 0.2), we did not include the
+ name of the cookie in the cookie signature. To read these old-style
+ cookies, pass include_name=False to this method. Otherwise, all
+ attempts to read old-style cookies will fail (and you may log all
+ your users out whose cookies were written with a previous Tornado
+ version).
+ """
+ if value is None: value = self.get_cookie(name)
+ if not value: return None
+ parts = value.split("|")
+ if len(parts) != 3: return None
+ if include_name:
+ signature = self._cookie_signature(name, parts[0], parts[1])
+ else:
+ signature = self._cookie_signature(parts[0], parts[1])
+ if not _time_independent_equals(parts[2], signature):
+ _log.warning("Invalid cookie signature %r", value)
+ return None
+ timestamp = int(parts[1])
+ if timestamp < time.time() - 31 * 86400:
+ _log.warning("Expired cookie %r", value)
+ return None
+ try:
+ return base64.b64decode(parts[0])
+ except:
+ return None
+
+ def _cookie_signature(self, *parts):
+ self.require_setting("cookie_secret", "secure cookies")
+ hash = hmac.new(self.application.settings["cookie_secret"],
+ digestmod=hashlib.sha1)
+ for part in parts: hash.update(part)
+ return hash.hexdigest()
+
+ def redirect(self, url, permanent=False):
+ """Sends a redirect to the given (optionally relative) URL."""
+ if self._headers_written:
+ raise Exception("Cannot redirect after headers have been written")
+ self.set_status(301 if permanent else 302)
+ # Remove whitespace
+ url = re.sub(r"[\x00-\x20]+", "", _utf8(url))
+ self.set_header("Location", urlparse.urljoin(self.request.uri, url))
+ self.finish()
+
+ def write(self, chunk):
+ """Writes the given chunk to the output buffer.
+
+ To write the output to the network, use the flush() method below.
+
+ If the given chunk is a dictionary, we write it as JSON and set
+ the Content-Type of the response to be text/javascript.
+ """
+ assert not self._finished
+ if isinstance(chunk, dict):
+ chunk = escape.json_encode(chunk)
+ self.set_header("Content-Type", "text/javascript; charset=UTF-8")
+ chunk = _utf8(chunk)
+ self._write_buffer.append(chunk)
+
+ def render(self, template_name, **kwargs):
+ """Renders the template with the given arguments as the response."""
+ html = self.render_string(template_name, **kwargs)
+
+ # Insert the additional JS and CSS added by the modules on the page
+ js_embed = []
+ js_files = []
+ css_embed = []
+ css_files = []
+ html_heads = []
+ html_bodies = []
+ for module in getattr(self, "_active_modules", {}).itervalues():
+ embed_part = module.embedded_javascript()
+ if embed_part: js_embed.append(_utf8(embed_part))
+ file_part = module.javascript_files()
+ if file_part:
+ if isinstance(file_part, basestring):
+ js_files.append(file_part)
+ else:
+ js_files.extend(file_part)
+ embed_part = module.embedded_css()
+ if embed_part: css_embed.append(_utf8(embed_part))
+ file_part = module.css_files()
+ if file_part:
+ if isinstance(file_part, basestring):
+ css_files.append(file_part)
+ else:
+ css_files.extend(file_part)
+ head_part = module.html_head()
+ if head_part: html_heads.append(_utf8(head_part))
+ body_part = module.html_body()
+ if body_part: html_bodies.append(_utf8(body_part))
+ if js_files:
+ # Maintain order of JavaScript files given by modules
+ paths = []
+ unique_paths = set()
+ for path in js_files:
+ if not path.startswith("/") and not path.startswith("http:"):
+ path = self.static_url(path)
+ if path not in unique_paths:
+ paths.append(path)
+ unique_paths.add(path)
+ js = ''.join('<script src="' + escape.xhtml_escape(p) +
+ '" type="text/javascript"></script>'
+ for p in paths)
+ sloc = html.rindex('</body>')
+ html = html[:sloc] + js + '\n' + html[sloc:]
+ if js_embed:
+ js = '<script type="text/javascript">\n//<![CDATA[\n' + \
+ '\n'.join(js_embed) + '\n//]]>\n</script>'
+ sloc = html.rindex('</body>')
+ html = html[:sloc] + js + '\n' + html[sloc:]
+ if css_files:
+ paths = set()
+ for path in css_files:
+ if not path.startswith("/") and not path.startswith("http:"):
+ paths.add(self.static_url(path))
+ else:
+ paths.add(path)
+ css = ''.join('<link href="' + escape.xhtml_escape(p) + '" '
+ 'type="text/css" rel="stylesheet"/>'
+ for p in paths)
+ hloc = html.index('</head>')
+ html = html[:hloc] + css + '\n' + html[hloc:]
+ if css_embed:
+ css = '<style type="text/css">\n' + '\n'.join(css_embed) + \
+ '\n</style>'
+ hloc = html.index('</head>')
+ html = html[:hloc] + css + '\n' + html[hloc:]
+ if html_heads:
+ hloc = html.index('</head>')
+ html = html[:hloc] + ''.join(html_heads) + '\n' + html[hloc:]
+ if html_bodies:
+ hloc = html.index('</body>')
+ html = html[:hloc] + ''.join(html_bodies) + '\n' + html[hloc:]
+ self.finish(html)
+
+ def render_string(self, template_name, **kwargs):
+ """Generate the given template with the given arguments.
+
+ We return the generated string. To generate and write a template
+ as a response, use render() above.
+ """
+ # If no template_path is specified, use the path of the calling file
+ template_path = self.application.settings.get("template_path")
+ if not template_path:
+ frame = sys._getframe(0)
+ web_file = frame.f_code.co_filename
+ while frame.f_code.co_filename == web_file:
+ frame = frame.f_back
+ template_path = os.path.dirname(frame.f_code.co_filename)
+ if not getattr(RequestHandler, "_templates", None):
+ RequestHandler._templates = {}
+ if template_path not in RequestHandler._templates:
+ loader = self.application.settings.get("template_loader") or\
+ template.Loader(template_path)
+ RequestHandler._templates[template_path] = loader
+ t = RequestHandler._templates[template_path].load(template_name)
+ args = dict(
+ handler=self,
+ request=self.request,
+ current_user=self.current_user,
+ locale=self.locale,
+ _=self.locale.translate,
+ static_url=self.static_url,
+ xsrf_form_html=self.xsrf_form_html,
+ reverse_url=self.application.reverse_url
+ )
+ args.update(self.ui)
+ args.update(kwargs)
+ return t.generate(**args)
+
+ def flush(self, include_footers=False):
+ """Flushes the current output buffer to the nextwork."""
+ if self.application._wsgi:
+ raise Exception("WSGI applications do not support flush()")
+
+ chunk = "".join(self._write_buffer)
+ self._write_buffer = []
+ if not self._headers_written:
+ self._headers_written = True
+ for transform in self._transforms:
+ self._headers, chunk = transform.transform_first_chunk(
+ self._headers, chunk, include_footers)
+ headers = self._generate_headers()
+ else:
+ for transform in self._transforms:
+ chunk = transform.transform_chunk(chunk, include_footers)
+ headers = ""
+
+ # Ignore the chunk and only write the headers for HEAD requests
+ if self.request.method == "HEAD":
+ if headers: self.request.write(headers)
+ return
+
+ if headers or chunk:
+ self.request.write(headers + chunk)
+
+ def finish(self, chunk=None):
+ """Finishes this response, ending the HTTP request."""
+ assert not self._finished
+ if chunk is not None: self.write(chunk)
+
+ # Automatically support ETags and add the Content-Length header if
+ # we have not flushed any content yet.
+ if not self._headers_written:
+ if (self._status_code == 200 and self.request.method == "GET" and
+ "Etag" not in self._headers):
+ hasher = hashlib.sha1()
+ for part in self._write_buffer:
+ hasher.update(part)
+ etag = '"%s"' % hasher.hexdigest()
+ inm = self.request.headers.get("If-None-Match")
+ if inm and inm.find(etag) != -1:
+ self._write_buffer = []
+ self.set_status(304)
+ else:
+ self.set_header("Etag", etag)
+ if "Content-Length" not in self._headers:
+ content_length = sum(len(part) for part in self._write_buffer)
+ self.set_header("Content-Length", content_length)
+
+ if not self.application._wsgi:
+ self.flush(include_footers=True)
+ self.request.finish()
+ self._log()
+ self._finished = True
+
+ def send_error(self, status_code=500, **kwargs):
+ """Sends the given HTTP error code to the browser.
+
+ We also send the error HTML for the given error code as returned by
+ get_error_html. Override that method if you want custom error pages
+ for your application.
+ """
+ if self._headers_written:
+ _log.error("Cannot send error response after headers written")
+ if not self._finished:
+ self.finish()
+ return
+ self.clear()
+ self.set_status(status_code)
+ message = self.get_error_html(status_code, **kwargs)
+ self.finish(message)
+
+ def get_error_html(self, status_code, **kwargs):
+ """Override to implement custom error pages.
+
+ If this error was caused by an uncaught exception, the
+ exception object can be found in kwargs e.g. kwargs['exception']
+ """
+ return "<html><title>%(code)d: %(message)s</title>" \
+ "<body>%(code)d: %(message)s</body></html>" % {
+ "code": status_code,
+ "message": httplib.responses[status_code],
+ }
+
+ @property
+ def locale(self):
+ """The local for the current session.
+
+ Determined by either get_user_locale, which you can override to
+ set the locale based on, e.g., a user preference stored in a
+ database, or get_browser_locale, which uses the Accept-Language
+ header.
+ """
+ if not hasattr(self, "_locale"):
+ self._locale = self.get_user_locale()
+ if not self._locale:
+ self._locale = self.get_browser_locale()
+ assert self._locale
+ return self._locale
+
+ def get_user_locale(self):
+ """Override to determine the locale from the authenticated user.
+
+ If None is returned, we use the Accept-Language header.
+ """
+ return None
+
+ def get_browser_locale(self, default="en_US"):
+ """Determines the user's locale from Accept-Language header.
+
+ See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4
+ """
+ if "Accept-Language" in self.request.headers:
+ languages = self.request.headers["Accept-Language"].split(",")
+ locales = []
+ for language in languages:
+ parts = language.strip().split(";")
+ if len(parts) > 1 and parts[1].startswith("q="):
+ try:
+ score = float(parts[1][2:])
+ except (ValueError, TypeError):
+ score = 0.0
+ else:
+ score = 1.0
+ locales.append((parts[0], score))
+ if locales:
+ locales.sort(key=lambda (l, s): s, reverse=True)
+ codes = [l[0] for l in locales]
+ return locale.get(*codes)
+ return locale.get(default)
+
+ @property
+ def current_user(self):
+ """The authenticated user for this request.
+
+ Determined by either get_current_user, which you can override to
+ set the user based on, e.g., a cookie. If that method is not
+ overridden, this method always returns None.
+
+ We lazy-load the current user the first time this method is called
+ and cache the result after that.
+ """
+ if not hasattr(self, "_current_user"):
+ self._current_user = self.get_current_user()
+ return self._current_user
+
+ def get_current_user(self):
+ """Override to determine the current user from, e.g., a cookie."""
+ return None
+
+ def get_login_url(self):
+ """Override to customize the login URL based on the request.
+
+ By default, we use the 'login_url' application setting.
+ """
+ self.require_setting("login_url", "@tornado.web.authenticated")
+ return self.application.settings["login_url"]
+
+ @property
+ def xsrf_token(self):
+ """The XSRF-prevention token for the current user/session.
+
+ To prevent cross-site request forgery, we set an '_xsrf' cookie
+ and include the same '_xsrf' value as an argument with all POST
+ requests. If the two do not match, we reject the form submission
+ as a potential forgery.
+
+ See http://en.wikipedia.org/wiki/Cross-site_request_forgery
+ """
+ if not hasattr(self, "_xsrf_token"):
+ token = self.get_cookie("_xsrf")
+ if not token:
+ token = binascii.b2a_hex(uuid.uuid4().bytes)
+ expires_days = 30 if self.current_user else None
+ self.set_cookie("_xsrf", token, expires_days=expires_days)
+ self._xsrf_token = token
+ return self._xsrf_token
+
+ def check_xsrf_cookie(self):
+ """Verifies that the '_xsrf' cookie matches the '_xsrf' argument.
+
+ To prevent cross-site request forgery, we set an '_xsrf' cookie
+ and include the same '_xsrf' value as an argument with all POST
+ requests. If the two do not match, we reject the form submission
+ as a potential forgery.
+
+ See http://en.wikipedia.org/wiki/Cross-site_request_forgery
+ """
+ if self.request.headers.get("X-Requested-With") == "XMLHttpRequest":
+ return
+ token = self.get_argument("_xsrf", None)
+ if not token:
+ raise HTTPError(403, "'_xsrf' argument missing from POST")
+ if self.xsrf_token != token:
+ raise HTTPError(403, "XSRF cookie does not match POST argument")
+
+ def xsrf_form_html(self):
+ """An HTML <input/> element to be included with all POST forms.
+
+ It defines the _xsrf input value, which we check on all POST
+ requests to prevent cross-site request forgery. If you have set
+ the 'xsrf_cookies' application setting, you must include this
+ HTML within all of your HTML forms.
+
+ See check_xsrf_cookie() above for more information.
+ """
+ return '<input type="hidden" name="_xsrf" value="' + \
+ escape.xhtml_escape(self.xsrf_token) + '"/>'
+
+ def static_url(self, path):
+ """Returns a static URL for the given relative static file path.
+
+ This method requires you set the 'static_path' setting in your
+ application (which specifies the root directory of your static
+ files).
+
+ We append ?v=<signature> to the returned URL, which makes our
+ static file handler set an infinite expiration header on the
+ returned content. The signature is based on the content of the
+ file.
+
+ If this handler has a "include_host" attribute, we include the
+ full host for every static URL, including the "http://". Set
+ this attribute for handlers whose output needs non-relative static
+ path names.
+ """
+ self.require_setting("static_path", "static_url")
+ if not hasattr(RequestHandler, "_static_hashes"):
+ RequestHandler._static_hashes = {}
+ hashes = RequestHandler._static_hashes
+ if path not in hashes:
+ try:
+ f = open(os.path.join(
+ self.application.settings["static_path"], path))
+ hashes[path] = hashlib.md5(f.read()).hexdigest()
+ f.close()
+ except:
+ _log.error("Could not open static file %r", path)
+ hashes[path] = None
+ base = self.request.protocol + "://" + self.request.host \
+ if getattr(self, "include_host", False) else ""
+ static_url_prefix = self.settings.get('static_url_prefix', '/static/')
+ if hashes.get(path):
+ return base + static_url_prefix + path + "?v=" + hashes[path][:5]
+ else:
+ return base + static_url_prefix + path
+
+ def async_callback(self, callback, *args, **kwargs):
+ """Wrap callbacks with this if they are used on asynchronous requests.
+
+ Catches exceptions and properly finishes the request.
+ """
+ if callback is None:
+ return None
+ if args or kwargs:
+ callback = functools.partial(callback, *args, **kwargs)
+ def wrapper(*args, **kwargs):
+ try:
+ return callback(*args, **kwargs)
+ except Exception, e:
+ if self._headers_written:
+ _log.error("Exception after headers written",
+ exc_info=True)
+ else:
+ self._handle_request_exception(e)
+ return wrapper
+
+ def require_setting(self, name, feature="this feature"):
+ """Raises an exception if the given app setting is not defined."""
+ if not self.application.settings.get(name):
+ raise Exception("You must define the '%s' setting in your "
+ "application to use %s" % (name, feature))
+
+ def reverse_url(self, name, *args):
+ return self.application.reverse_url(name, *args)
+
+ def _execute(self, transforms, *args, **kwargs):
+ """Executes this request with the given output transforms."""
+ self._transforms = transforms
+ try:
+ if self.request.method not in self.SUPPORTED_METHODS:
+ raise HTTPError(405)
+ # If XSRF cookies are turned on, reject form submissions without
+ # the proper cookie
+ if self.request.method == "POST" and \
+ self.application.settings.get("xsrf_cookies"):
+ self.check_xsrf_cookie()
+ self.prepare()
+ if not self._finished:
+ getattr(self, self.request.method.lower())(*args, **kwargs)
+ if self._auto_finish and not self._finished:
+ self.finish()
+ except Exception, e:
+ self._handle_request_exception(e)
+
+ def _generate_headers(self):
+ lines = [self.request.version + " " + str(self._status_code) + " " +
+ httplib.responses[self._status_code]]
+ lines.extend(["%s: %s" % (n, v) for n, v in self._headers.iteritems()])
+ for cookie_dict in getattr(self, "_new_cookies", []):
+ for cookie in cookie_dict.values():
+ lines.append("Set-Cookie: " + cookie.OutputString(None))
+ return "\r\n".join(lines) + "\r\n\r\n"
+
+ def _log(self):
+ if self._status_code < 400:
+ log_method = _log.info
+ elif self._status_code < 500:
+ log_method = _log.warning
+ else:
+ log_method = _log.error
+ request_time = 1000.0 * self.request.request_time()
+ log_method("%d %s %.2fms", self._status_code,
+ self._request_summary(), request_time)
+
+ def _request_summary(self):
+ return self.request.method + " " + self.request.uri + " (" + \
+ self.request.remote_ip + ")"
+
+ def _handle_request_exception(self, e):
+ if isinstance(e, HTTPError):
+ if e.log_message:
+ format = "%d %s: " + e.log_message
+ args = [e.status_code, self._request_summary()] + list(e.args)
+ _log.warning(format, *args)
+ if e.status_code not in httplib.responses:
+ _log.error("Bad HTTP status code: %d", e.status_code)
+ self.send_error(500, exception=e)
+ else:
+ self.send_error(e.status_code, exception=e)
+ else:
+ _log.error("Uncaught exception %s\n%r", self._request_summary(),
+ self.request, exc_info=e)
+ self.send_error(500, exception=e)
+
+ def _ui_module(self, name, module):
+ def render(*args, **kwargs):
+ if not hasattr(self, "_active_modules"):
+ self._active_modules = {}
+ if name not in self._active_modules:
+ self._active_modules[name] = module(self)
+ rendered = self._active_modules[name].render(*args, **kwargs)
+ return rendered
+ return render
+
+ def _ui_method(self, method):
+ return lambda *args, **kwargs: method(self, *args, **kwargs)
+
+
+def asynchronous(method):
+ """Wrap request handler methods with this if they are asynchronous.
+
+ If this decorator is given, the response is not finished when the
+ method returns. It is up to the request handler to call self.finish()
+ to finish the HTTP request. Without this decorator, the request is
+ automatically finished when the get() or post() method returns.
+
+ class MyRequestHandler(web.RequestHandler):
+ @web.asynchronous
+ def get(self):
+ http = httpclient.AsyncHTTPClient()
+ http.fetch("http://friendfeed.com/", self._on_download)
+
+ def _on_download(self, response):
+ self.write("Downloaded!")
+ self.finish()
+
+ """
+ @functools.wraps(method)
+ def wrapper(self, *args, **kwargs):
+ if self.application._wsgi:
+ raise Exception("@asynchronous is not supported for WSGI apps")
+ self._auto_finish = False
+ return method(self, *args, **kwargs)
+ return wrapper
+
+
+def removeslash(method):
+ """Use this decorator to remove trailing slashes from the request path.
+
+ For example, a request to '/foo/' would redirect to '/foo' with this
+ decorator. Your request handler mapping should use a regular expression
+ like r'/foo/*' in conjunction with using the decorator.
+ """
+ @functools.wraps(method)
+ def wrapper(self, *args, **kwargs):
+ if self.request.path.endswith("/"):
+ if self.request.method == "GET":
+ uri = self.request.path.rstrip("/")
+ if self.request.query: uri += "?" + self.request.query
+ self.redirect(uri)
+ return
+ raise HTTPError(404)
+ return method(self, *args, **kwargs)
+ return wrapper
+
+
+def addslash(method):
+ """Use this decorator to add a missing trailing slash to the request path.
+
+ For example, a request to '/foo' would redirect to '/foo/' with this
+ decorator. Your request handler mapping should use a regular expression
+ like r'/foo/?' in conjunction with using the decorator.
+ """
+ @functools.wraps(method)
+ def wrapper(self, *args, **kwargs):
+ if not self.request.path.endswith("/"):
+ if self.request.method == "GET":
+ uri = self.request.path + "/"
+ if self.request.query: uri += "?" + self.request.query
+ self.redirect(uri)
+ return
+ raise HTTPError(404)
+ return method(self, *args, **kwargs)
+ return wrapper
+
+
+class Application(object):
+ """A collection of request handlers that make up a web application.
+
+ Instances of this class are callable and can be passed directly to
+ HTTPServer to serve the application:
+
+ application = web.Application([
+ (r"/", MainPageHandler),
+ ])
+ http_server = httpserver.HTTPServer(application)
+ http_server.listen(8080)
+ ioloop.IOLoop.instance().start()
+
+ The constructor for this class takes in a list of URLSpec objects
+ or (regexp, request_class) tuples. When we receive requests, we
+ iterate over the list in order and instantiate an instance of the
+ first request class whose regexp matches the request path.
+
+ Each tuple can contain an optional third element, which should be a
+ dictionary if it is present. That dictionary is passed as keyword
+ arguments to the contructor of the handler. This pattern is used
+ for the StaticFileHandler below:
+
+ application = web.Application([
+ (r"/static/(.*)", web.StaticFileHandler, {"path": "/var/www"}),
+ ])
+
+ We support virtual hosts with the add_handlers method, which takes in
+ a host regular expression as the first argument:
+
+ application.add_handlers(r"www\.myhost\.com", [
+ (r"/article/([0-9]+)", ArticleHandler),
+ ])
+
+ You can serve static files by sending the static_path setting as a
+ keyword argument. We will serve those files from the /static/ URI
+ (this is configurable with the static_url_prefix setting),
+ and we will serve /favicon.ico and /robots.txt from the same directory.
+ """
+ def __init__(self, handlers=None, default_host="", transforms=None,
+ wsgi=False, **settings):
+ if transforms is None:
+ self.transforms = []
+ if settings.get("gzip"):
+ self.transforms.append(GZipContentEncoding)
+ self.transforms.append(ChunkedTransferEncoding)
+ else:
+ self.transforms = transforms
+ self.handlers = []
+ self.named_handlers = {}
+ self.default_host = default_host
+ self.settings = settings
+ self.ui_modules = {}
+ self.ui_methods = {}
+ self._wsgi = wsgi
+ self._load_ui_modules(settings.get("ui_modules", {}))
+ self._load_ui_methods(settings.get("ui_methods", {}))
+ if self.settings.get("static_path"):
+ path = self.settings["static_path"]
+ handlers = list(handlers or [])
+ static_url_prefix = settings.get("static_url_prefix",
+ "/static/")
+ handlers = [
+ (re.escape(static_url_prefix) + r"(.*)", StaticFileHandler,
+ dict(path=path)),
+ (r"/(favicon\.ico)", StaticFileHandler, dict(path=path)),
+ (r"/(robots\.txt)", StaticFileHandler, dict(path=path)),
+ ] + handlers
+ if handlers: self.add_handlers(".*$", handlers)
+
+ # Automatically reload modified modules
+ if self.settings.get("debug") and not wsgi:
+ import autoreload
+ autoreload.start()
+
+ def add_handlers(self, host_pattern, host_handlers):
+ """Appends the given handlers to our handler list."""
+ if not host_pattern.endswith("$"):
+ host_pattern += "$"
+ handlers = []
+ # The handlers with the wildcard host_pattern are a special
+ # case - they're added in the constructor but should have lower
+ # precedence than the more-precise handlers added later.
+ # If a wildcard handler group exists, it should always be last
+ # in the list, so insert new groups just before it.
+ if self.handlers and self.handlers[-1][0].pattern == '.*$':
+ self.handlers.insert(-1, (re.compile(host_pattern), handlers))
+ else:
+ self.handlers.append((re.compile(host_pattern), handlers))
+
+ for spec in host_handlers:
+ if type(spec) is type(()):
+ assert len(spec) in (2, 3)
+ pattern = spec[0]
+ handler = spec[1]
+ if len(spec) == 3:
+ kwargs = spec[2]
+ else:
+ kwargs = {}
+ spec = URLSpec(pattern, handler, kwargs)
+ handlers.append(spec)
+ if spec.name:
+ if spec.name in self.named_handlers:
+ _log.warning(
+ "Multiple handlers named %s; replacing previous value",
+ spec.name)
+ self.named_handlers[spec.name] = spec
+
+ def add_transform(self, transform_class):
+ """Adds the given OutputTransform to our transform list."""
+ self.transforms.append(transform_class)
+
+ def _get_host_handlers(self, request):
+ host = request.host.lower().split(':')[0]
+ for pattern, handlers in self.handlers:
+ if pattern.match(host):
+ return handlers
+ # Look for default host if not behind load balancer (for debugging)
+ if "X-Real-Ip" not in request.headers:
+ for pattern, handlers in self.handlers:
+ if pattern.match(self.default_host):
+ return handlers
+ return None
+
+ def _load_ui_methods(self, methods):
+ if type(methods) is types.ModuleType:
+ self._load_ui_methods(dict((n, getattr(methods, n))
+ for n in dir(methods)))
+ elif isinstance(methods, list):
+ for m in list: self._load_ui_methods(m)
+ else:
+ for name, fn in methods.iteritems():
+ if not name.startswith("_") and hasattr(fn, "__call__") \
+ and name[0].lower() == name[0]:
+ self.ui_methods[name] = fn
+
+ def _load_ui_modules(self, modules):
+ if type(modules) is types.ModuleType:
+ self._load_ui_modules(dict((n, getattr(modules, n))
+ for n in dir(modules)))
+ elif isinstance(modules, list):
+ for m in list: self._load_ui_modules(m)
+ else:
+ assert isinstance(modules, dict)
+ for name, cls in modules.iteritems():
+ try:
+ if issubclass(cls, UIModule):
+ self.ui_modules[name] = cls
+ except TypeError:
+ pass
+
+ def __call__(self, request):
+ """Called by HTTPServer to execute the request."""
+ transforms = [t(request) for t in self.transforms]
+ handler = None
+ args = []
+ kwargs = {}
+ handlers = self._get_host_handlers(request)
+ if not handlers:
+ handler = RedirectHandler(
+ request, "http://" + self.default_host + "/")
+ else:
+ for spec in handlers:
+ match = spec.regex.match(request.path)
+ if match:
+ handler = spec.handler_class(self, request, **spec.kwargs)
+ # Pass matched groups to the handler. Since
+ # match.groups() includes both named and unnamed groups,
+ # we want to use either groups or groupdict but not both.
+ kwargs = match.groupdict()
+ if kwargs:
+ args = []
+ else:
+ args = match.groups()
+ break
+ if not handler:
+ handler = ErrorHandler(self, request, 404)
+
+ # In debug mode, re-compile templates and reload static files on every
+ # request so you don't need to restart to see changes
+ if self.settings.get("debug"):
+ if getattr(RequestHandler, "_templates", None):
+ map(lambda loader: loader.reset(),
+ RequestHandler._templates.values())
+ RequestHandler._static_hashes = {}
+
+ handler._execute(transforms, *args, **kwargs)
+ return handler
+
+ def reverse_url(self, name, *args):
+ """Returns a URL path for handler named `name`
+
+ The handler must be added to the application as a named URLSpec
+ """
+ if name in self.named_handlers:
+ return self.named_handlers[name].reverse(*args)
+ raise KeyError("%s not found in named urls" % name)
+
+
+class HTTPError(Exception):
+ """An exception that will turn into an HTTP error response."""
+ def __init__(self, status_code, log_message=None, *args):
+ self.status_code = status_code
+ self.log_message = log_message
+ self.args = args
+
+ def __str__(self):
+ message = "HTTP %d: %s" % (
+ self.status_code, httplib.responses[self.status_code])
+ if self.log_message:
+ return message + " (" + (self.log_message % self.args) + ")"
+ else:
+ return message
+
+
+class ErrorHandler(RequestHandler):
+ """Generates an error response with status_code for all requests."""
+ def __init__(self, application, request, status_code):
+ RequestHandler.__init__(self, application, request)
+ self.set_status(status_code)
+
+ def prepare(self):
+ raise HTTPError(self._status_code)
+
+
+class RedirectHandler(RequestHandler):
+ """Redirects the client to the given URL for all GET requests.
+
+ You should provide the keyword argument "url" to the handler, e.g.:
+
+ application = web.Application([
+ (r"/oldpath", web.RedirectHandler, {"url": "/newpath"}),
+ ])
+ """
+ def __init__(self, application, request, url, permanent=True):
+ RequestHandler.__init__(self, application, request)
+ self._url = url
+ self._permanent = permanent
+
+ def get(self):
+ self.redirect(self._url, permanent=self._permanent)
+
+
+class StaticFileHandler(RequestHandler):
+ """A simple handler that can serve static content from a directory.
+
+ To map a path to this handler for a static data directory /var/www,
+ you would add a line to your application like:
+
+ application = web.Application([
+ (r"/static/(.*)", web.StaticFileHandler, {"path": "/var/www"}),
+ ])
+
+ The local root directory of the content should be passed as the "path"
+ argument to the handler.
+
+ To support aggressive browser caching, if the argument "v" is given
+ with the path, we set an infinite HTTP expiration header. So, if you
+ want browsers to cache a file indefinitely, send them to, e.g.,
+ /static/images/myimage.png?v=xxx.
+ """
+ def __init__(self, application, request, path):
+ RequestHandler.__init__(self, application, request)
+ self.root = os.path.abspath(path) + os.path.sep
+
+ def head(self, path):
+ self.get(path, include_body=False)
+
+ def get(self, path, include_body=True):
+ abspath = os.path.abspath(os.path.join(self.root, path))
+ if not abspath.startswith(self.root):
+ raise HTTPError(403, "%s is not in root static directory", path)
+ if not os.path.exists(abspath):
+ raise HTTPError(404)
+ if not os.path.isfile(abspath):
+ raise HTTPError(403, "%s is not a file", path)
+
+ stat_result = os.stat(abspath)
+ modified = datetime.datetime.fromtimestamp(stat_result[stat.ST_MTIME])
+
+ self.set_header("Last-Modified", modified)
+ if "v" in self.request.arguments:
+ self.set_header("Expires", datetime.datetime.utcnow() + \
+ datetime.timedelta(days=365*10))
+ self.set_header("Cache-Control", "max-age=" + str(86400*365*10))
+ else:
+ self.set_header("Cache-Control", "public")
+ mime_type, encoding = mimetypes.guess_type(abspath)
+ if mime_type:
+ self.set_header("Content-Type", mime_type)
+
+ # Check the If-Modified-Since, and don't send the result if the
+ # content has not been modified
+ ims_value = self.request.headers.get("If-Modified-Since")
+ if ims_value is not None:
+ date_tuple = email.utils.parsedate(ims_value)
+ if_since = datetime.datetime.fromtimestamp(time.mktime(date_tuple))
+ if if_since >= modified:
+ self.set_status(304)
+ return
+
+ if not include_body:
+ return
+ self.set_header("Content-Length", stat_result[stat.ST_SIZE])
+ file = open(abspath, "rb")
+ try:
+ self.write(file.read())
+ finally:
+ file.close()
+
+
+class FallbackHandler(RequestHandler):
+ """A RequestHandler that wraps another HTTP server callback.
+
+ The fallback is a callable object that accepts an HTTPRequest,
+ such as an Application or tornado.wsgi.WSGIContainer. This is most
+ useful to use both tornado RequestHandlers and WSGI in the same server.
+ Typical usage:
+ wsgi_app = tornado.wsgi.WSGIContainer(
+ django.core.handlers.wsgi.WSGIHandler())
+ application = tornado.web.Application([
+ (r"/foo", FooHandler),
+ (r".*", FallbackHandler, dict(fallback=wsgi_app),
+ ])
+ """
+ def __init__(self, app, request, fallback):
+ RequestHandler.__init__(self, app, request)
+ self.fallback = fallback
+
+ def prepare(self):
+ self.fallback(self.request)
+ self._finished = True
+
+
+class OutputTransform(object):
+ """A transform modifies the result of an HTTP request (e.g., GZip encoding)
+
+ A new transform instance is created for every request. See the
+ ChunkedTransferEncoding example below if you want to implement a
+ new Transform.
+ """
+ def __init__(self, request):
+ pass
+
+ def transform_first_chunk(self, headers, chunk, finishing):
+ return headers, chunk
+
+ def transform_chunk(self, chunk, finishing):
+ return chunk
+
+
+class GZipContentEncoding(OutputTransform):
+ """Applies the gzip content encoding to the response.
+
+ See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.11
+ """
+ CONTENT_TYPES = set([
+ "text/plain", "text/html", "text/css", "text/xml",
+ "application/x-javascript", "application/xml", "application/atom+xml",
+ "text/javascript", "application/json", "application/xhtml+xml"])
+ MIN_LENGTH = 5
+
+ def __init__(self, request):
+ self._gzipping = request.supports_http_1_1() and \
+ "gzip" in request.headers.get("Accept-Encoding", "")
+
+ def transform_first_chunk(self, headers, chunk, finishing):
+ if self._gzipping:
+ ctype = headers.get("Content-Type", "").split(";")[0]
+ self._gzipping = (ctype in self.CONTENT_TYPES) and \
+ (not finishing or len(chunk) >= self.MIN_LENGTH) and \
+ (finishing or "Content-Length" not in headers) and \
+ ("Content-Encoding" not in headers)
+ if self._gzipping:
+ headers["Content-Encoding"] = "gzip"
+ self._gzip_value = cStringIO.StringIO()
+ self._gzip_file = gzip.GzipFile(mode="w", fileobj=self._gzip_value)
+ self._gzip_pos = 0
+ chunk = self.transform_chunk(chunk, finishing)
+ if "Content-Length" in headers:
+ headers["Content-Length"] = str(len(chunk))
+ return headers, chunk
+
+ def transform_chunk(self, chunk, finishing):
+ if self._gzipping:
+ self._gzip_file.write(chunk)
+ if finishing:
+ self._gzip_file.close()
+ else:
+ self._gzip_file.flush()
+ chunk = self._gzip_value.getvalue()
+ if self._gzip_pos > 0:
+ chunk = chunk[self._gzip_pos:]
+ self._gzip_pos += len(chunk)
+ return chunk
+
+
+class ChunkedTransferEncoding(OutputTransform):
+ """Applies the chunked transfer encoding to the response.
+
+ See http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.6.1
+ """
+ def __init__(self, request):
+ self._chunking = request.supports_http_1_1()
+
+ def transform_first_chunk(self, headers, chunk, finishing):
+ if self._chunking:
+ # No need to chunk the output if a Content-Length is specified
+ if "Content-Length" in headers or "Transfer-Encoding" in headers:
+ self._chunking = False
+ else:
+ headers["Transfer-Encoding"] = "chunked"
+ chunk = self.transform_chunk(chunk, finishing)
+ return headers, chunk
+
+ def transform_chunk(self, block, finishing):
+ if self._chunking:
+ # Don't write out empty chunks because that means END-OF-STREAM
+ # with chunked encoding
+ if block:
+ block = ("%x" % len(block)) + "\r\n" + block + "\r\n"
+ if finishing:
+ block += "0\r\n\r\n"
+ return block
+
+
+def authenticated(method):
+ """Decorate methods with this to require that the user be logged in."""
+ @functools.wraps(method)
+ def wrapper(self, *args, **kwargs):
+ if not self.current_user:
+ if self.request.method == "GET":
+ url = self.get_login_url()
+ if "?" not in url:
+ url += "?" + urllib.urlencode(dict(next=self.request.uri))
+ self.redirect(url)
+ return
+ raise HTTPError(403)
+ return method(self, *args, **kwargs)
+ return wrapper
+
+
+class UIModule(object):
+ """A UI re-usable, modular unit on a page.
+
+ UI modules often execute additional queries, and they can include
+ additional CSS and JavaScript that will be included in the output
+ page, which is automatically inserted on page render.
+ """
+ def __init__(self, handler):
+ self.handler = handler
+ self.request = handler.request
+ self.ui = handler.ui
+ self.current_user = handler.current_user
+ self.locale = handler.locale
+
+ def render(self, *args, **kwargs):
+ raise NotImplementedError()
+
+ def embedded_javascript(self):
+ """Returns a JavaScript string that will be embedded in the page."""
+ return None
+
+ def javascript_files(self):
+ """Returns a list of JavaScript files required by this module."""
+ return None
+
+ def embedded_css(self):
+ """Returns a CSS string that will be embedded in the page."""
+ return None
+
+ def css_files(self):
+ """Returns a list of JavaScript files required by this module."""
+ return None
+
+ def html_head(self):
+ """Returns a CSS string that will be put in the <head/> element"""
+ return None
+
+ def html_body(self):
+ """Returns an HTML string that will be put in the <body/> element"""
+ return None
+
+ def render_string(self, path, **kwargs):
+ return self.handler.render_string(path, **kwargs)
+
+class URLSpec(object):
+ """Specifies mappings between URLs and handlers."""
+ def __init__(self, pattern, handler_class, kwargs={}, name=None):
+ """Creates a URLSpec.
+
+ Parameters:
+ pattern: Regular expression to be matched. Any groups in the regex
+ will be passed in to the handler's get/post/etc methods as
+ arguments.
+ handler_class: RequestHandler subclass to be invoked.
+ kwargs (optional): A dictionary of additional arguments to be passed
+ to the handler's constructor.
+ name (optional): A name for this handler. Used by
+ Application.reverse_url.
+ """
+ if not pattern.endswith('$'):
+ pattern += '$'
+ self.regex = re.compile(pattern)
+ self.handler_class = handler_class
+ self.kwargs = kwargs
+ self.name = name
+ self._path, self._group_count = self._find_groups()
+
+ def _find_groups(self):
+ """Returns a tuple (reverse string, group count) for a url.
+
+ For example: Given the url pattern /([0-9]{4})/([a-z-]+)/, this method
+ would return ('/%s/%s/', 2).
+ """
+ pattern = self.regex.pattern
+ if pattern.startswith('^'):
+ pattern = pattern[1:]
+ if pattern.endswith('$'):
+ pattern = pattern[:-1]
+
+ if self.regex.groups != pattern.count('('):
+ # The pattern is too complicated for our simplistic matching,
+ # so we can't support reversing it.
+ return (None, None)
+
+ pieces = []
+ for fragment in pattern.split('('):
+ if ')' in fragment:
+ paren_loc = fragment.index(')')
+ if paren_loc >= 0:
+ pieces.append('%s' + fragment[paren_loc + 1:])
+ else:
+ pieces.append(fragment)
+
+ return (''.join(pieces), self.regex.groups)
+
+ def reverse(self, *args):
+ assert self._path is not None, \
+ "Cannot reverse url regex " + self.regex.pattern
+ assert len(args) == self._group_count, "required number of arguments "\
+ "not found"
+ if not len(args):
+ return self._path
+ return self._path % tuple([str(a) for a in args])
+
+url = URLSpec
+
+def _utf8(s):
+ if isinstance(s, unicode):
+ return s.encode("utf-8")
+ assert isinstance(s, str)
+ return s
+
+
+def _unicode(s):
+ if isinstance(s, str):
+ try:
+ return s.decode("utf-8")
+ except UnicodeDecodeError:
+ raise HTTPError(400, "Non-utf8 argument")
+ assert isinstance(s, unicode)
+ return s
+
+
+def _time_independent_equals(a, b):
+ if len(a) != len(b):
+ return False
+ result = 0
+ for x, y in zip(a, b):
+ result |= ord(x) ^ ord(y)
+ return result == 0
+
+
+class _O(dict):
+ """Makes a dictionary behave like an object."""
+ def __getattr__(self, name):
+ try:
+ return self[name]
+ except KeyError:
+ raise AttributeError(name)
+
+ def __setattr__(self, name, value):
+ self[name] = value
diff --git a/vendor/tornado/tornado/websocket.py b/vendor/tornado/tornado/websocket.py
new file mode 100644
index 0000000000..38a58012cc
--- /dev/null
+++ b/vendor/tornado/tornado/websocket.py
@@ -0,0 +1,138 @@
+#!/usr/bin/env python
+#
+# Copyright 2009 Facebook
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import functools
+import logging
+import tornado.escape
+import tornado.web
+
+_log = logging.getLogger('tornado.websocket')
+
+class WebSocketHandler(tornado.web.RequestHandler):
+ """A request handler for HTML 5 Web Sockets.
+
+ See http://www.w3.org/TR/2009/WD-websockets-20091222/ for details on the
+ JavaScript interface. We implement the protocol as specified at
+ http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-55.
+
+ Here is an example Web Socket handler that echos back all received messages
+ back to the client:
+
+ class EchoWebSocket(websocket.WebSocketHandler):
+ def open(self):
+ self.receive_message(self.on_message)
+
+ def on_message(self, message):
+ self.write_message(u"You said: " + message)
+
+ Web Sockets are not standard HTTP connections. The "handshake" is HTTP,
+ but after the handshake, the protocol is message-based. Consequently,
+ most of the Tornado HTTP facilities are not available in handlers of this
+ type. The only communication methods available to you are send_message()
+ and receive_message(). Likewise, your request handler class should
+ implement open() method rather than get() or post().
+
+ If you map the handler above to "/websocket" in your application, you can
+ invoke it in JavaScript with:
+
+ var ws = new WebSocket("ws://localhost:8888/websocket");
+ ws.onopen = function() {
+ ws.send("Hello, world");
+ };
+ ws.onmessage = function (evt) {
+ alert(evt.data);
+ };
+
+ This script pops up an alert box that says "You said: Hello, world".
+ """
+ def __init__(self, application, request):
+ tornado.web.RequestHandler.__init__(self, application, request)
+ self.stream = request.connection.stream
+
+ def _execute(self, transforms, *args, **kwargs):
+ if self.request.headers.get("Upgrade") != "WebSocket" or \
+ self.request.headers.get("Connection") != "Upgrade" or \
+ not self.request.headers.get("Origin"):
+ message = "Expected WebSocket headers"
+ self.stream.write(
+ "HTTP/1.1 403 Forbidden\r\nContent-Length: " +
+ str(len(message)) + "\r\n\r\n" + message)
+ return
+ self.stream.write(
+ "HTTP/1.1 101 Web Socket Protocol Handshake\r\n"
+ "Upgrade: WebSocket\r\n"
+ "Connection: Upgrade\r\n"
+ "Server: TornadoServer/0.1\r\n"
+ "WebSocket-Origin: " + self.request.headers["Origin"] + "\r\n"
+ "WebSocket-Location: ws://" + self.request.host +
+ self.request.path + "\r\n\r\n")
+ self.async_callback(self.open)(*args, **kwargs)
+
+ def write_message(self, message):
+ """Sends the given message to the client of this Web Socket."""
+ if isinstance(message, dict):
+ message = tornado.escape.json_encode(message)
+ if isinstance(message, unicode):
+ message = message.encode("utf-8")
+ assert isinstance(message, str)
+ self.stream.write("\x00" + message + "\xff")
+
+ def receive_message(self, callback):
+ """Calls callback when the browser calls send() on this Web Socket."""
+ callback = self.async_callback(callback)
+ self.stream.read_bytes(
+ 1, functools.partial(self._on_frame_type, callback))
+
+ def close(self):
+ """Closes this Web Socket.
+
+ The browser will receive the onclose event for the open web socket
+ when this method is called.
+ """
+ self.stream.close()
+
+ def async_callback(self, callback, *args, **kwargs):
+ """Wrap callbacks with this if they are used on asynchronous requests.
+
+ Catches exceptions properly and closes this Web Socket if an exception
+ is uncaught.
+ """
+ if args or kwargs:
+ callback = functools.partial(callback, *args, **kwargs)
+ def wrapper(*args, **kwargs):
+ try:
+ return callback(*args, **kwargs)
+ except Exception, e:
+ _log.error("Uncaught exception in %s",
+ self.request.path, exc_info=True)
+ self.stream.close()
+ return wrapper
+
+ def _on_frame_type(self, callback, byte):
+ if ord(byte) & 0x80 == 0x80:
+ raise Exception("Length-encoded format not yet supported")
+ self.stream.read_until(
+ "\xff", functools.partial(self._on_end_delimiter, callback))
+
+ def _on_end_delimiter(self, callback, frame):
+ callback(frame[:-1].decode("utf-8", "replace"))
+
+ def _not_supported(self, *args, **kwargs):
+ raise Exception("Method not supported for Web Sockets")
+
+for method in ["write", "redirect", "set_header", "send_error", "set_cookie",
+ "set_status", "flush", "finish"]:
+ setattr(WebSocketHandler, method, WebSocketHandler._not_supported)
diff --git a/vendor/tornado/tornado/win32_support.py b/vendor/tornado/tornado/win32_support.py
new file mode 100644
index 0000000000..f3efa8e892
--- /dev/null
+++ b/vendor/tornado/tornado/win32_support.py
@@ -0,0 +1,123 @@
+# NOTE: win32 support is currently experimental, and not recommended
+# for production use.
+
+import ctypes
+import ctypes.wintypes
+import os
+import socket
+import errno
+
+
+# See: http://msdn.microsoft.com/en-us/library/ms738573(VS.85).aspx
+ioctlsocket = ctypes.windll.ws2_32.ioctlsocket
+ioctlsocket.argtypes = (ctypes.wintypes.HANDLE, ctypes.wintypes.LONG, ctypes.wintypes.ULONG)
+ioctlsocket.restype = ctypes.c_int
+
+# See: http://msdn.microsoft.com/en-us/library/ms724935(VS.85).aspx
+SetHandleInformation = ctypes.windll.kernel32.SetHandleInformation
+SetHandleInformation.argtypes = (ctypes.wintypes.HANDLE, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD)
+SetHandleInformation.restype = ctypes.wintypes.BOOL
+
+HANDLE_FLAG_INHERIT = 0x00000001
+
+
+F_GETFD = 1
+F_SETFD = 2
+F_GETFL = 3
+F_SETFL = 4
+
+FD_CLOEXEC = 1
+
+os.O_NONBLOCK = 2048
+
+FIONBIO = 126
+
+
+def fcntl(fd, op, arg=0):
+ if op == F_GETFD or op == F_GETFL:
+ return 0
+ elif op == F_SETFD:
+ # Check that the flag is CLOEXEC and translate
+ if arg == FD_CLOEXEC:
+ success = SetHandleInformation(fd, HANDLE_FLAG_INHERIT, arg)
+ if not success:
+ raise ctypes.GetLastError()
+ else:
+ raise ValueError("Unsupported arg")
+ #elif op == F_SETFL:
+ ## Check that the flag is NONBLOCK and translate
+ #if arg == os.O_NONBLOCK:
+ ##pass
+ #result = ioctlsocket(fd, FIONBIO, 1)
+ #if result != 0:
+ #raise ctypes.GetLastError()
+ #else:
+ #raise ValueError("Unsupported arg")
+ else:
+ raise ValueError("Unsupported op")
+
+
+class Pipe(object):
+ """Create an OS independent asynchronous pipe"""
+ def __init__(self):
+ # Based on Zope async.py: http://svn.zope.org/zc.ngi/trunk/src/zc/ngi/async.py
+
+ self.writer = socket.socket()
+ # Disable buffering -- pulling the trigger sends 1 byte,
+ # and we want that sent immediately, to wake up ASAP.
+ self.writer.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
+
+ count = 0
+ while 1:
+ count += 1
+ # Bind to a local port; for efficiency, let the OS pick
+ # a free port for us.
+ # Unfortunately, stress tests showed that we may not
+ # be able to connect to that port ("Address already in
+ # use") despite that the OS picked it. This appears
+ # to be a race bug in the Windows socket implementation.
+ # So we loop until a connect() succeeds (almost always
+ # on the first try). See the long thread at
+ # http://mail.zope.org/pipermail/zope/2005-July/160433.html
+ # for hideous details.
+ a = socket.socket()
+ a.bind(("127.0.0.1", 0))
+ connect_address = a.getsockname() # assigned (host, port) pair
+ a.listen(1)
+ try:
+ self.writer.connect(connect_address)
+ break # success
+ except socket.error, detail:
+ if detail[0] != errno.WSAEADDRINUSE:
+ # "Address already in use" is the only error
+ # I've seen on two WinXP Pro SP2 boxes, under
+ # Pythons 2.3.5 and 2.4.1.
+ raise
+ # (10048, 'Address already in use')
+ # assert count <= 2 # never triggered in Tim's tests
+ if count >= 10: # I've never seen it go above 2
+ a.close()
+ self.writer.close()
+ raise socket.error("Cannot bind trigger!")
+ # Close `a` and try again. Note: I originally put a short
+ # sleep() here, but it didn't appear to help or hurt.
+ a.close()
+
+ self.reader, addr = a.accept()
+ self.reader.setblocking(0)
+ self.writer.setblocking(0)
+ a.close()
+ self.reader_fd = self.reader.fileno()
+
+ def read(self):
+ """Emulate a file descriptors read method"""
+ try:
+ return self.reader.recv(1)
+ except socket.error, ex:
+ if ex.args[0] == errno.EWOULDBLOCK:
+ raise IOError
+ raise
+
+ def write(self, data):
+ """Emulate a file descriptors write method"""
+ return self.writer.send(data)
diff --git a/vendor/tornado/tornado/wsgi.py b/vendor/tornado/tornado/wsgi.py
new file mode 100644
index 0000000000..69fa0988eb
--- /dev/null
+++ b/vendor/tornado/tornado/wsgi.py
@@ -0,0 +1,311 @@
+#!/usr/bin/env python
+#
+# Copyright 2009 Facebook
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""WSGI support for the Tornado web framework.
+
+We export WSGIApplication, which is very similar to web.Application, except
+no asynchronous methods are supported (since WSGI does not support
+non-blocking requests properly). If you call self.flush() or other
+asynchronous methods in your request handlers running in a WSGIApplication,
+we throw an exception.
+
+Example usage:
+
+ import tornado.web
+ import tornado.wsgi
+ import wsgiref.simple_server
+
+ class MainHandler(tornado.web.RequestHandler):
+ def get(self):
+ self.write("Hello, world")
+
+ if __name__ == "__main__":
+ application = tornado.wsgi.WSGIApplication([
+ (r"/", MainHandler),
+ ])
+ server = wsgiref.simple_server.make_server('', 8888, application)
+ server.serve_forever()
+
+See the 'appengine' demo for an example of using this module to run
+a Tornado app on Google AppEngine.
+
+Since no asynchronous methods are available for WSGI applications, the
+httpclient and auth modules are both not available for WSGI applications.
+
+We also export WSGIContainer, which lets you run other WSGI-compatible
+frameworks on the Tornado HTTP server and I/O loop. See WSGIContainer for
+details and documentation.
+"""
+
+import cgi
+import cStringIO
+import escape
+import httplib
+import logging
+import sys
+import time
+import urllib
+import web
+
+_log = logging.getLogger('tornado.wsgi')
+
+class WSGIApplication(web.Application):
+ """A WSGI-equivalent of web.Application.
+
+ We support the same interface, but handlers running in a WSGIApplication
+ do not support flush() or asynchronous methods.
+ """
+ def __init__(self, handlers=None, default_host="", **settings):
+ web.Application.__init__(self, handlers, default_host, transforms=[],
+ wsgi=True, **settings)
+
+ def __call__(self, environ, start_response):
+ handler = web.Application.__call__(self, HTTPRequest(environ))
+ assert handler._finished
+ status = str(handler._status_code) + " " + \
+ httplib.responses[handler._status_code]
+ headers = handler._headers.items()
+ for cookie_dict in getattr(handler, "_new_cookies", []):
+ for cookie in cookie_dict.values():
+ headers.append(("Set-Cookie", cookie.OutputString(None)))
+ start_response(status, headers)
+ return handler._write_buffer
+
+
+class HTTPRequest(object):
+ """Mimics httpserver.HTTPRequest for WSGI applications."""
+ def __init__(self, environ):
+ """Parses the given WSGI environ to construct the request."""
+ self.method = environ["REQUEST_METHOD"]
+ self.path = urllib.quote(environ.get("SCRIPT_NAME", ""))
+ self.path += urllib.quote(environ.get("PATH_INFO", ""))
+ self.uri = self.path
+ self.arguments = {}
+ self.query = environ.get("QUERY_STRING", "")
+ if self.query:
+ self.uri += "?" + self.query
+ arguments = cgi.parse_qs(self.query)
+ for name, values in arguments.iteritems():
+ values = [v for v in values if v]
+ if values: self.arguments[name] = values
+ self.version = "HTTP/1.1"
+ self.headers = HTTPHeaders()
+ if environ.get("CONTENT_TYPE"):
+ self.headers["Content-Type"] = environ["CONTENT_TYPE"]
+ if environ.get("CONTENT_LENGTH"):
+ self.headers["Content-Length"] = int(environ["CONTENT_LENGTH"])
+ for key in environ:
+ if key.startswith("HTTP_"):
+ self.headers[key[5:].replace("_", "-")] = environ[key]
+ if self.headers.get("Content-Length"):
+ self.body = environ["wsgi.input"].read()
+ else:
+ self.body = ""
+ self.protocol = environ["wsgi.url_scheme"]
+ self.remote_ip = environ.get("REMOTE_ADDR", "")
+ if environ.get("HTTP_HOST"):
+ self.host = environ["HTTP_HOST"]
+ else:
+ self.host = environ["SERVER_NAME"]
+
+ # Parse request body
+ self.files = {}
+ content_type = self.headers.get("Content-Type", "")
+ if content_type.startswith("application/x-www-form-urlencoded"):
+ for name, values in cgi.parse_qs(self.body).iteritems():
+ self.arguments.setdefault(name, []).extend(values)
+ elif content_type.startswith("multipart/form-data"):
+ boundary = content_type[30:]
+ if boundary: self._parse_mime_body(boundary)
+
+ self._start_time = time.time()
+ self._finish_time = None
+
+ def supports_http_1_1(self):
+ """Returns True if this request supports HTTP/1.1 semantics"""
+ return self.version == "HTTP/1.1"
+
+ def full_url(self):
+ """Reconstructs the full URL for this request."""
+ return self.protocol + "://" + self.host + self.uri
+
+ def request_time(self):
+ """Returns the amount of time it took for this request to execute."""
+ if self._finish_time is None:
+ return time.time() - self._start_time
+ else:
+ return self._finish_time - self._start_time
+
+ def _parse_mime_body(self, boundary):
+ if self.body.endswith("\r\n"):
+ footer_length = len(boundary) + 6
+ else:
+ footer_length = len(boundary) + 4
+ parts = self.body[:-footer_length].split("--" + boundary + "\r\n")
+ for part in parts:
+ if not part: continue
+ eoh = part.find("\r\n\r\n")
+ if eoh == -1:
+ _log.warning("multipart/form-data missing headers")
+ continue
+ headers = HTTPHeaders.parse(part[:eoh])
+ name_header = headers.get("Content-Disposition", "")
+ if not name_header.startswith("form-data;") or \
+ not part.endswith("\r\n"):
+ _log.warning("Invalid multipart/form-data")
+ continue
+ value = part[eoh + 4:-2]
+ name_values = {}
+ for name_part in name_header[10:].split(";"):
+ name, name_value = name_part.strip().split("=", 1)
+ name_values[name] = name_value.strip('"').decode("utf-8")
+ if not name_values.get("name"):
+ _log.warning("multipart/form-data value missing name")
+ continue
+ name = name_values["name"]
+ if name_values.get("filename"):
+ ctype = headers.get("Content-Type", "application/unknown")
+ self.files.setdefault(name, []).append(dict(
+ filename=name_values["filename"], body=value,
+ content_type=ctype))
+ else:
+ self.arguments.setdefault(name, []).append(value)
+
+
+class WSGIContainer(object):
+ """Makes a WSGI-compatible function runnable on Tornado's HTTP server.
+
+ Wrap a WSGI function in a WSGIContainer and pass it to HTTPServer to
+ run it. For example:
+
+ def simple_app(environ, start_response):
+ status = "200 OK"
+ response_headers = [("Content-type", "text/plain")]
+ start_response(status, response_headers)
+ return ["Hello world!\n"]
+
+ container = tornado.wsgi.WSGIContainer(simple_app)
+ http_server = tornado.httpserver.HTTPServer(container)
+ http_server.listen(8888)
+ tornado.ioloop.IOLoop.instance().start()
+
+ This class is intended to let other frameworks (Django, web.py, etc)
+ run on the Tornado HTTP server and I/O loop. It has not yet been
+ thoroughly tested in production.
+ """
+ def __init__(self, wsgi_application):
+ self.wsgi_application = wsgi_application
+
+ def __call__(self, request):
+ data = {}
+ response = []
+ def start_response(status, response_headers, exc_info=None):
+ data["status"] = status
+ data["headers"] = response_headers
+ return response.append
+ response.extend(self.wsgi_application(
+ WSGIContainer.environ(request), start_response))
+ body = "".join(response)
+ if hasattr(response, "close"):
+ response.close()
+ if not data: raise Exception("WSGI app did not call start_response")
+
+ status_code = int(data["status"].split()[0])
+ headers = data["headers"]
+ header_set = set(k.lower() for (k,v) in headers)
+ body = escape.utf8(body)
+ if "content-length" not in header_set:
+ headers.append(("Content-Length", str(len(body))))
+ if "content-type" not in header_set:
+ headers.append(("Content-Type", "text/html; charset=UTF-8"))
+ if "server" not in header_set:
+ headers.append(("Server", "TornadoServer/0.1"))
+
+ parts = ["HTTP/1.1 " + data["status"] + "\r\n"]
+ for key, value in headers:
+ parts.append(escape.utf8(key) + ": " + escape.utf8(value) + "\r\n")
+ parts.append("\r\n")
+ parts.append(body)
+ request.write("".join(parts))
+ request.finish()
+ self._log(status_code, request)
+
+ @staticmethod
+ def environ(request):
+ hostport = request.host.split(":")
+ if len(hostport) == 2:
+ host = hostport[0]
+ port = int(hostport[1])
+ else:
+ host = request.host
+ port = 443 if request.protocol == "https" else 80
+ environ = {
+ "REQUEST_METHOD": request.method,
+ "SCRIPT_NAME": "",
+ "PATH_INFO": request.path,
+ "QUERY_STRING": request.query,
+ "REMOTE_ADDR": request.remote_ip,
+ "SERVER_NAME": host,
+ "SERVER_PORT": port,
+ "SERVER_PROTOCOL": request.version,
+ "wsgi.version": (1, 0),
+ "wsgi.url_scheme": request.protocol,
+ "wsgi.input": cStringIO.StringIO(request.body),
+ "wsgi.errors": sys.stderr,
+ "wsgi.multithread": False,
+ "wsgi.multiprocess": True,
+ "wsgi.run_once": False,
+ }
+ if "Content-Type" in request.headers:
+ environ["CONTENT_TYPE"] = request.headers["Content-Type"]
+ if "Content-Length" in request.headers:
+ environ["CONTENT_LENGTH"] = request.headers["Content-Length"]
+ for key, value in request.headers.iteritems():
+ environ["HTTP_" + key.replace("-", "_").upper()] = value
+ return environ
+
+ def _log(self, status_code, request):
+ if status_code < 400:
+ log_method = _log.info
+ elif status_code < 500:
+ log_method = _log.warning
+ else:
+ log_method = _log.error
+ request_time = 1000.0 * request.request_time()
+ summary = request.method + " " + request.uri + " (" + \
+ request.remote_ip + ")"
+ log_method("%d %s %.2fms", status_code, summary, request_time)
+
+
+class HTTPHeaders(dict):
+ """A dictionary that maintains Http-Header-Case for all keys."""
+ def __setitem__(self, name, value):
+ dict.__setitem__(self, self._normalize_name(name), value)
+
+ def __getitem__(self, name):
+ return dict.__getitem__(self, self._normalize_name(name))
+
+ def _normalize_name(self, name):
+ return "-".join([w.capitalize() for w in name.split("-")])
+
+ @classmethod
+ def parse(cls, headers_string):
+ headers = cls()
+ for line in headers_string.splitlines():
+ if line:
+ name, value = line.split(": ", 1)
+ headers[name] = value
+ return headers
diff --git a/vendor/tornado/website/app.yaml b/vendor/tornado/website/app.yaml
new file mode 100644
index 0000000000..8a1ff06648
--- /dev/null
+++ b/vendor/tornado/website/app.yaml
@@ -0,0 +1,15 @@
+application: python-tornado
+version: 1
+runtime: python
+api_version: 1
+
+handlers:
+- url: /static/
+ static_dir: static
+
+- url: /robots\.txt
+ static_files: static/robots.txt
+ upload: static/robots.txt
+
+- url: /.*
+ script: website.py
diff --git a/vendor/tornado/website/index.yaml b/vendor/tornado/website/index.yaml
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/vendor/tornado/website/index.yaml
diff --git a/vendor/tornado/website/markdown/__init__.py b/vendor/tornado/website/markdown/__init__.py
new file mode 100644
index 0000000000..0d1c504979
--- /dev/null
+++ b/vendor/tornado/website/markdown/__init__.py
@@ -0,0 +1,603 @@
+"""
+Python Markdown
+===============
+
+Python Markdown converts Markdown to HTML and can be used as a library or
+called from the command line.
+
+## Basic usage as a module:
+
+ import markdown
+ md = Markdown()
+ html = md.convert(your_text_string)
+
+## Basic use from the command line:
+
+ python markdown.py source.txt > destination.html
+
+Run "python markdown.py --help" to see more options.
+
+## Extensions
+
+See <http://www.freewisdom.org/projects/python-markdown/> for more
+information and instructions on how to extend the functionality of
+Python Markdown. Read that before you try modifying this file.
+
+## Authors and License
+
+Started by [Manfred Stienstra](http://www.dwerg.net/). Continued and
+maintained by [Yuri Takhteyev](http://www.freewisdom.org), [Waylan
+Limberg](http://achinghead.com/) and [Artem Yunusov](http://blog.splyer.com).
+
+Contact: markdown@freewisdom.org
+
+Copyright 2007, 2008 The Python Markdown Project (v. 1.7 and later)
+Copyright 200? Django Software Foundation (OrderedDict implementation)
+Copyright 2004, 2005, 2006 Yuri Takhteyev (v. 0.2-1.6b)
+Copyright 2004 Manfred Stienstra (the original version)
+
+License: BSD (see docs/LICENSE for details).
+"""
+
+version = "2.0"
+version_info = (2,0,0, "Final")
+
+import re
+import codecs
+import sys
+import warnings
+import logging
+from logging import DEBUG, INFO, WARN, ERROR, CRITICAL
+
+
+"""
+CONSTANTS
+=============================================================================
+"""
+
+"""
+Constants you might want to modify
+-----------------------------------------------------------------------------
+"""
+
+# default logging level for command-line use
+COMMAND_LINE_LOGGING_LEVEL = CRITICAL
+TAB_LENGTH = 4 # expand tabs to this many spaces
+ENABLE_ATTRIBUTES = True # @id = xyz -> <... id="xyz">
+SMART_EMPHASIS = True # this_or_that does not become this<i>or</i>that
+DEFAULT_OUTPUT_FORMAT = 'xhtml1' # xhtml or html4 output
+HTML_REMOVED_TEXT = "[HTML_REMOVED]" # text used instead of HTML in safe mode
+BLOCK_LEVEL_ELEMENTS = re.compile("p|div|h[1-6]|blockquote|pre|table|dl|ol|ul"
+ "|script|noscript|form|fieldset|iframe|math"
+ "|ins|del|hr|hr/|style|li|dt|dd|thead|tbody"
+ "|tr|th|td")
+DOC_TAG = "div" # Element used to wrap document - later removed
+
+# Placeholders
+STX = u'\u0002' # Use STX ("Start of text") for start-of-placeholder
+ETX = u'\u0003' # Use ETX ("End of text") for end-of-placeholder
+INLINE_PLACEHOLDER_PREFIX = STX+"klzzwxh:"
+INLINE_PLACEHOLDER = INLINE_PLACEHOLDER_PREFIX + "%s" + ETX
+AMP_SUBSTITUTE = STX+"amp"+ETX
+
+
+"""
+Constants you probably do not need to change
+-----------------------------------------------------------------------------
+"""
+
+RTL_BIDI_RANGES = ( (u'\u0590', u'\u07FF'),
+ # Hebrew (0590-05FF), Arabic (0600-06FF),
+ # Syriac (0700-074F), Arabic supplement (0750-077F),
+ # Thaana (0780-07BF), Nko (07C0-07FF).
+ (u'\u2D30', u'\u2D7F'), # Tifinagh
+ )
+
+
+"""
+AUXILIARY GLOBAL FUNCTIONS
+=============================================================================
+"""
+
+
+def message(level, text):
+ """ A wrapper method for logging debug messages. """
+ logger = logging.getLogger('MARKDOWN')
+ if logger.handlers:
+ # The logger is configured
+ logger.log(level, text)
+ if level > WARN:
+ sys.exit(0)
+ elif level > WARN:
+ raise MarkdownException, text
+ else:
+ warnings.warn(text, MarkdownWarning)
+
+
+def isBlockLevel(tag):
+ """Check if the tag is a block level HTML tag."""
+ return BLOCK_LEVEL_ELEMENTS.match(tag)
+
+"""
+MISC AUXILIARY CLASSES
+=============================================================================
+"""
+
+class AtomicString(unicode):
+ """A string which should not be further processed."""
+ pass
+
+
+class MarkdownException(Exception):
+ """ A Markdown Exception. """
+ pass
+
+
+class MarkdownWarning(Warning):
+ """ A Markdown Warning. """
+ pass
+
+
+"""
+OVERALL DESIGN
+=============================================================================
+
+Markdown processing takes place in four steps:
+
+1. A bunch of "preprocessors" munge the input text.
+2. BlockParser() parses the high-level structural elements of the
+ pre-processed text into an ElementTree.
+3. A bunch of "treeprocessors" are run against the ElementTree. One such
+ treeprocessor runs InlinePatterns against the ElementTree, detecting inline
+ markup.
+4. Some post-processors are run against the text after the ElementTree has
+ been serialized into text.
+5. The output is written to a string.
+
+Those steps are put together by the Markdown() class.
+
+"""
+
+import preprocessors
+import blockprocessors
+import treeprocessors
+import inlinepatterns
+import postprocessors
+import blockparser
+import etree_loader
+import odict
+
+# Extensions should use "markdown.etree" instead of "etree" (or do `from
+# markdown import etree`). Do not import it by yourself.
+
+etree = etree_loader.importETree()
+
+# Adds the ability to output html4
+import html4
+
+
+class Markdown:
+ """Convert Markdown to HTML."""
+
+ def __init__(self,
+ extensions=[],
+ extension_configs={},
+ safe_mode = False,
+ output_format=DEFAULT_OUTPUT_FORMAT):
+ """
+ Creates a new Markdown instance.
+
+ Keyword arguments:
+
+ * extensions: A list of extensions.
+ If they are of type string, the module mdx_name.py will be loaded.
+ If they are a subclass of markdown.Extension, they will be used
+ as-is.
+ * extension-configs: Configuration setting for extensions.
+ * safe_mode: Disallow raw html. One of "remove", "replace" or "escape".
+ * output_format: Format of output. Supported formats are:
+ * "xhtml1": Outputs XHTML 1.x. Default.
+ * "xhtml": Outputs latest supported version of XHTML (currently XHTML 1.1).
+ * "html4": Outputs HTML 4
+ * "html": Outputs latest supported version of HTML (currently HTML 4).
+ Note that it is suggested that the more specific formats ("xhtml1"
+ and "html4") be used as "xhtml" or "html" may change in the future
+ if it makes sense at that time.
+
+ """
+
+ self.safeMode = safe_mode
+ self.registeredExtensions = []
+ self.docType = ""
+ self.stripTopLevelTags = True
+
+ # Preprocessors
+ self.preprocessors = odict.OrderedDict()
+ self.preprocessors["html_block"] = \
+ preprocessors.HtmlBlockPreprocessor(self)
+ self.preprocessors["reference"] = \
+ preprocessors.ReferencePreprocessor(self)
+ # footnote preprocessor will be inserted with "<reference"
+
+ # Block processors - ran by the parser
+ self.parser = blockparser.BlockParser()
+ self.parser.blockprocessors['empty'] = \
+ blockprocessors.EmptyBlockProcessor(self.parser)
+ self.parser.blockprocessors['indent'] = \
+ blockprocessors.ListIndentProcessor(self.parser)
+ self.parser.blockprocessors['code'] = \
+ blockprocessors.CodeBlockProcessor(self.parser)
+ self.parser.blockprocessors['hashheader'] = \
+ blockprocessors.HashHeaderProcessor(self.parser)
+ self.parser.blockprocessors['setextheader'] = \
+ blockprocessors.SetextHeaderProcessor(self.parser)
+ self.parser.blockprocessors['hr'] = \
+ blockprocessors.HRProcessor(self.parser)
+ self.parser.blockprocessors['olist'] = \
+ blockprocessors.OListProcessor(self.parser)
+ self.parser.blockprocessors['ulist'] = \
+ blockprocessors.UListProcessor(self.parser)
+ self.parser.blockprocessors['quote'] = \
+ blockprocessors.BlockQuoteProcessor(self.parser)
+ self.parser.blockprocessors['paragraph'] = \
+ blockprocessors.ParagraphProcessor(self.parser)
+
+
+ #self.prePatterns = []
+
+ # Inline patterns - Run on the tree
+ self.inlinePatterns = odict.OrderedDict()
+ self.inlinePatterns["backtick"] = \
+ inlinepatterns.BacktickPattern(inlinepatterns.BACKTICK_RE)
+ self.inlinePatterns["escape"] = \
+ inlinepatterns.SimpleTextPattern(inlinepatterns.ESCAPE_RE)
+ self.inlinePatterns["reference"] = \
+ inlinepatterns.ReferencePattern(inlinepatterns.REFERENCE_RE, self)
+ self.inlinePatterns["link"] = \
+ inlinepatterns.LinkPattern(inlinepatterns.LINK_RE, self)
+ self.inlinePatterns["image_link"] = \
+ inlinepatterns.ImagePattern(inlinepatterns.IMAGE_LINK_RE, self)
+ self.inlinePatterns["image_reference"] = \
+ inlinepatterns.ImageReferencePattern(inlinepatterns.IMAGE_REFERENCE_RE, self)
+ self.inlinePatterns["autolink"] = \
+ inlinepatterns.AutolinkPattern(inlinepatterns.AUTOLINK_RE, self)
+ self.inlinePatterns["automail"] = \
+ inlinepatterns.AutomailPattern(inlinepatterns.AUTOMAIL_RE, self)
+ self.inlinePatterns["linebreak2"] = \
+ inlinepatterns.SubstituteTagPattern(inlinepatterns.LINE_BREAK_2_RE, 'br')
+ self.inlinePatterns["linebreak"] = \
+ inlinepatterns.SubstituteTagPattern(inlinepatterns.LINE_BREAK_RE, 'br')
+ self.inlinePatterns["html"] = \
+ inlinepatterns.HtmlPattern(inlinepatterns.HTML_RE, self)
+ self.inlinePatterns["entity"] = \
+ inlinepatterns.HtmlPattern(inlinepatterns.ENTITY_RE, self)
+ self.inlinePatterns["not_strong"] = \
+ inlinepatterns.SimpleTextPattern(inlinepatterns.NOT_STRONG_RE)
+ self.inlinePatterns["strong_em"] = \
+ inlinepatterns.DoubleTagPattern(inlinepatterns.STRONG_EM_RE, 'strong,em')
+ self.inlinePatterns["strong"] = \
+ inlinepatterns.SimpleTagPattern(inlinepatterns.STRONG_RE, 'strong')
+ self.inlinePatterns["emphasis"] = \
+ inlinepatterns.SimpleTagPattern(inlinepatterns.EMPHASIS_RE, 'em')
+ self.inlinePatterns["emphasis2"] = \
+ inlinepatterns.SimpleTagPattern(inlinepatterns.EMPHASIS_2_RE, 'em')
+ # The order of the handlers matters!!!
+
+
+ # Tree processors - run once we have a basic parse.
+ self.treeprocessors = odict.OrderedDict()
+ self.treeprocessors["inline"] = treeprocessors.InlineProcessor(self)
+ self.treeprocessors["prettify"] = \
+ treeprocessors.PrettifyTreeprocessor(self)
+
+ # Postprocessors - finishing touches.
+ self.postprocessors = odict.OrderedDict()
+ self.postprocessors["raw_html"] = \
+ postprocessors.RawHtmlPostprocessor(self)
+ self.postprocessors["amp_substitute"] = \
+ postprocessors.AndSubstitutePostprocessor()
+ # footnote postprocessor will be inserted with ">amp_substitute"
+
+ # Map format keys to serializers
+ self.output_formats = {
+ 'html' : html4.to_html_string,
+ 'html4' : html4.to_html_string,
+ 'xhtml' : etree.tostring,
+ 'xhtml1': etree.tostring,
+ }
+
+ self.references = {}
+ self.htmlStash = preprocessors.HtmlStash()
+ self.registerExtensions(extensions = extensions,
+ configs = extension_configs)
+ self.set_output_format(output_format)
+ self.reset()
+
+ def registerExtensions(self, extensions, configs):
+ """
+ Register extensions with this instance of Markdown.
+
+ Keyword aurguments:
+
+ * extensions: A list of extensions, which can either
+ be strings or objects. See the docstring on Markdown.
+ * configs: A dictionary mapping module names to config options.
+
+ """
+ for ext in extensions:
+ if isinstance(ext, basestring):
+ ext = load_extension(ext, configs.get(ext, []))
+ try:
+ ext.extendMarkdown(self, globals())
+ except AttributeError:
+ message(ERROR, "Incorrect type! Extension '%s' is "
+ "neither a string or an Extension." %(repr(ext)))
+
+
+ def registerExtension(self, extension):
+ """ This gets called by the extension """
+ self.registeredExtensions.append(extension)
+
+ def reset(self):
+ """
+ Resets all state variables so that we can start with a new text.
+ """
+ self.htmlStash.reset()
+ self.references.clear()
+
+ for extension in self.registeredExtensions:
+ extension.reset()
+
+ def set_output_format(self, format):
+ """ Set the output format for the class instance. """
+ try:
+ self.serializer = self.output_formats[format.lower()]
+ except KeyError:
+ message(CRITICAL, 'Invalid Output Format: "%s". Use one of %s.' \
+ % (format, self.output_formats.keys()))
+
+ def convert(self, source):
+ """
+ Convert markdown to serialized XHTML or HTML.
+
+ Keyword arguments:
+
+ * source: Source text as a Unicode string.
+
+ """
+
+ # Fixup the source text
+ if not source.strip():
+ return u"" # a blank unicode string
+ try:
+ source = unicode(source)
+ except UnicodeDecodeError:
+ message(CRITICAL, 'UnicodeDecodeError: Markdown only accepts unicode or ascii input.')
+ return u""
+
+ source = source.replace(STX, "").replace(ETX, "")
+ source = source.replace("\r\n", "\n").replace("\r", "\n") + "\n\n"
+ source = re.sub(r'\n\s+\n', '\n\n', source)
+ source = source.expandtabs(TAB_LENGTH)
+
+ # Split into lines and run the line preprocessors.
+ self.lines = source.split("\n")
+ for prep in self.preprocessors.values():
+ self.lines = prep.run(self.lines)
+
+ # Parse the high-level elements.
+ root = self.parser.parseDocument(self.lines).getroot()
+
+ # Run the tree-processors
+ for treeprocessor in self.treeprocessors.values():
+ newRoot = treeprocessor.run(root)
+ if newRoot:
+ root = newRoot
+
+ # Serialize _properly_. Strip top-level tags.
+ output, length = codecs.utf_8_decode(self.serializer(root, encoding="utf8"))
+ if self.stripTopLevelTags:
+ start = output.index('<%s>'%DOC_TAG)+len(DOC_TAG)+2
+ end = output.rindex('</%s>'%DOC_TAG)
+ output = output[start:end].strip()
+
+ # Run the text post-processors
+ for pp in self.postprocessors.values():
+ output = pp.run(output)
+
+ return output.strip()
+
+ def convertFile(self, input=None, output=None, encoding=None):
+ """Converts a markdown file and returns the HTML as a unicode string.
+
+ Decodes the file using the provided encoding (defaults to utf-8),
+ passes the file content to markdown, and outputs the html to either
+ the provided stream or the file with provided name, using the same
+ encoding as the source file.
+
+ **Note:** This is the only place that decoding and encoding of unicode
+ takes place in Python-Markdown. (All other code is unicode-in /
+ unicode-out.)
+
+ Keyword arguments:
+
+ * input: Name of source text file.
+ * output: Name of output file. Writes to stdout if `None`.
+ * encoding: Encoding of input and output files. Defaults to utf-8.
+
+ """
+
+ encoding = encoding or "utf-8"
+
+ # Read the source
+ input_file = codecs.open(input, mode="r", encoding=encoding)
+ text = input_file.read()
+ input_file.close()
+ text = text.lstrip(u'\ufeff') # remove the byte-order mark
+
+ # Convert
+ html = self.convert(text)
+
+ # Write to file or stdout
+ if isinstance(output, (str, unicode)):
+ output_file = codecs.open(output, "w", encoding=encoding)
+ output_file.write(html)
+ output_file.close()
+ else:
+ output.write(html.encode(encoding))
+
+
+"""
+Extensions
+-----------------------------------------------------------------------------
+"""
+
+class Extension:
+ """ Base class for extensions to subclass. """
+ def __init__(self, configs = {}):
+ """Create an instance of an Extention.
+
+ Keyword arguments:
+
+ * configs: A dict of configuration setting used by an Extension.
+ """
+ self.config = configs
+
+ def getConfig(self, key):
+ """ Return a setting for the given key or an empty string. """
+ if key in self.config:
+ return self.config[key][0]
+ else:
+ return ""
+
+ def getConfigInfo(self):
+ """ Return all config settings as a list of tuples. """
+ return [(key, self.config[key][1]) for key in self.config.keys()]
+
+ def setConfig(self, key, value):
+ """ Set a config setting for `key` with the given `value`. """
+ self.config[key][0] = value
+
+ def extendMarkdown(self, md, md_globals):
+ """
+ Add the various proccesors and patterns to the Markdown Instance.
+
+ This method must be overriden by every extension.
+
+ Keyword arguments:
+
+ * md: The Markdown instance.
+
+ * md_globals: Global variables in the markdown module namespace.
+
+ """
+ pass
+
+
+def load_extension(ext_name, configs = []):
+ """Load extension by name, then return the module.
+
+ The extension name may contain arguments as part of the string in the
+ following format: "extname(key1=value1,key2=value2)"
+
+ """
+
+ # Parse extensions config params (ignore the order)
+ configs = dict(configs)
+ pos = ext_name.find("(") # find the first "("
+ if pos > 0:
+ ext_args = ext_name[pos+1:-1]
+ ext_name = ext_name[:pos]
+ pairs = [x.split("=") for x in ext_args.split(",")]
+ configs.update([(x.strip(), y.strip()) for (x, y) in pairs])
+
+ # Setup the module names
+ ext_module = 'markdown.extensions'
+ module_name_new_style = '.'.join([ext_module, ext_name])
+ module_name_old_style = '_'.join(['mdx', ext_name])
+
+ # Try loading the extention first from one place, then another
+ try: # New style (markdown.extensons.<extension>)
+ module = __import__(module_name_new_style, {}, {}, [ext_module])
+ except ImportError:
+ try: # Old style (mdx.<extension>)
+ module = __import__(module_name_old_style)
+ except ImportError:
+ message(WARN, "Failed loading extension '%s' from '%s' or '%s'"
+ % (ext_name, module_name_new_style, module_name_old_style))
+ # Return None so we don't try to initiate none-existant extension
+ return None
+
+ # If the module is loaded successfully, we expect it to define a
+ # function called makeExtension()
+ try:
+ return module.makeExtension(configs.items())
+ except AttributeError:
+ message(CRITICAL, "Failed to initiate extension '%s'" % ext_name)
+
+
+def load_extensions(ext_names):
+ """Loads multiple extensions"""
+ extensions = []
+ for ext_name in ext_names:
+ extension = load_extension(ext_name)
+ if extension:
+ extensions.append(extension)
+ return extensions
+
+
+"""
+EXPORTED FUNCTIONS
+=============================================================================
+
+Those are the two functions we really mean to export: markdown() and
+markdownFromFile().
+"""
+
+def markdown(text,
+ extensions = [],
+ safe_mode = False,
+ output_format = DEFAULT_OUTPUT_FORMAT):
+ """Convert a markdown string to HTML and return HTML as a unicode string.
+
+ This is a shortcut function for `Markdown` class to cover the most
+ basic use case. It initializes an instance of Markdown, loads the
+ necessary extensions and runs the parser on the given text.
+
+ Keyword arguments:
+
+ * text: Markdown formatted text as Unicode or ASCII string.
+ * extensions: A list of extensions or extension names (may contain config args).
+ * safe_mode: Disallow raw html. One of "remove", "replace" or "escape".
+ * output_format: Format of output. Supported formats are:
+ * "xhtml1": Outputs XHTML 1.x. Default.
+ * "xhtml": Outputs latest supported version of XHTML (currently XHTML 1.1).
+ * "html4": Outputs HTML 4
+ * "html": Outputs latest supported version of HTML (currently HTML 4).
+ Note that it is suggested that the more specific formats ("xhtml1"
+ and "html4") be used as "xhtml" or "html" may change in the future
+ if it makes sense at that time.
+
+ Returns: An HTML document as a string.
+
+ """
+ md = Markdown(extensions=load_extensions(extensions),
+ safe_mode=safe_mode,
+ output_format=output_format)
+ return md.convert(text)
+
+
+def markdownFromFile(input = None,
+ output = None,
+ extensions = [],
+ encoding = None,
+ safe_mode = False,
+ output_format = DEFAULT_OUTPUT_FORMAT):
+ """Read markdown code from a file and write it to a file or a stream."""
+ md = Markdown(extensions=load_extensions(extensions),
+ safe_mode=safe_mode,
+ output_format=output_format)
+ md.convertFile(input, output, encoding)
+
+
+
diff --git a/vendor/tornado/website/markdown/blockparser.py b/vendor/tornado/website/markdown/blockparser.py
new file mode 100644
index 0000000000..e18b338487
--- /dev/null
+++ b/vendor/tornado/website/markdown/blockparser.py
@@ -0,0 +1,95 @@
+
+import markdown
+
+class State(list):
+ """ Track the current and nested state of the parser.
+
+ This utility class is used to track the state of the BlockParser and
+ support multiple levels if nesting. It's just a simple API wrapped around
+ a list. Each time a state is set, that state is appended to the end of the
+ list. Each time a state is reset, that state is removed from the end of
+ the list.
+
+ Therefore, each time a state is set for a nested block, that state must be
+ reset when we back out of that level of nesting or the state could be
+ corrupted.
+
+ While all the methods of a list object are available, only the three
+ defined below need be used.
+
+ """
+
+ def set(self, state):
+ """ Set a new state. """
+ self.append(state)
+
+ def reset(self):
+ """ Step back one step in nested state. """
+ self.pop()
+
+ def isstate(self, state):
+ """ Test that top (current) level is of given state. """
+ if len(self):
+ return self[-1] == state
+ else:
+ return False
+
+class BlockParser:
+ """ Parse Markdown blocks into an ElementTree object.
+
+ A wrapper class that stitches the various BlockProcessors together,
+ looping through them and creating an ElementTree object.
+ """
+
+ def __init__(self):
+ self.blockprocessors = markdown.odict.OrderedDict()
+ self.state = State()
+
+ def parseDocument(self, lines):
+ """ Parse a markdown document into an ElementTree.
+
+ Given a list of lines, an ElementTree object (not just a parent Element)
+ is created and the root element is passed to the parser as the parent.
+ The ElementTree object is returned.
+
+ This should only be called on an entire document, not pieces.
+
+ """
+ # Create a ElementTree from the lines
+ self.root = markdown.etree.Element(markdown.DOC_TAG)
+ self.parseChunk(self.root, '\n'.join(lines))
+ return markdown.etree.ElementTree(self.root)
+
+ def parseChunk(self, parent, text):
+ """ Parse a chunk of markdown text and attach to given etree node.
+
+ While the ``text`` argument is generally assumed to contain multiple
+ blocks which will be split on blank lines, it could contain only one
+ block. Generally, this method would be called by extensions when
+ block parsing is required.
+
+ The ``parent`` etree Element passed in is altered in place.
+ Nothing is returned.
+
+ """
+ self.parseBlocks(parent, text.split('\n\n'))
+
+ def parseBlocks(self, parent, blocks):
+ """ Process blocks of markdown text and attach to given etree node.
+
+ Given a list of ``blocks``, each blockprocessor is stepped through
+ until there are no blocks left. While an extension could potentially
+ call this method directly, it's generally expected to be used internally.
+
+ This is a public method as an extension may need to add/alter additional
+ BlockProcessors which call this method to recursively parse a nested
+ block.
+
+ """
+ while blocks:
+ for processor in self.blockprocessors.values():
+ if processor.test(parent, blocks[0]):
+ processor.run(parent, blocks)
+ break
+
+
diff --git a/vendor/tornado/website/markdown/blockprocessors.py b/vendor/tornado/website/markdown/blockprocessors.py
new file mode 100644
index 0000000000..79f4db93bc
--- /dev/null
+++ b/vendor/tornado/website/markdown/blockprocessors.py
@@ -0,0 +1,460 @@
+"""
+CORE MARKDOWN BLOCKPARSER
+=============================================================================
+
+This parser handles basic parsing of Markdown blocks. It doesn't concern itself
+with inline elements such as **bold** or *italics*, but rather just catches
+blocks, lists, quotes, etc.
+
+The BlockParser is made up of a bunch of BlockProssors, each handling a
+different type of block. Extensions may add/replace/remove BlockProcessors
+as they need to alter how markdown blocks are parsed.
+
+"""
+
+import re
+import markdown
+
+class BlockProcessor:
+ """ Base class for block processors.
+
+ Each subclass will provide the methods below to work with the source and
+ tree. Each processor will need to define it's own ``test`` and ``run``
+ methods. The ``test`` method should return True or False, to indicate
+ whether the current block should be processed by this processor. If the
+ test passes, the parser will call the processors ``run`` method.
+
+ """
+
+ def __init__(self, parser=None):
+ self.parser = parser
+
+ def lastChild(self, parent):
+ """ Return the last child of an etree element. """
+ if len(parent):
+ return parent[-1]
+ else:
+ return None
+
+ def detab(self, text):
+ """ Remove a tab from the front of each line of the given text. """
+ newtext = []
+ lines = text.split('\n')
+ for line in lines:
+ if line.startswith(' '*markdown.TAB_LENGTH):
+ newtext.append(line[markdown.TAB_LENGTH:])
+ elif not line.strip():
+ newtext.append('')
+ else:
+ break
+ return '\n'.join(newtext), '\n'.join(lines[len(newtext):])
+
+ def looseDetab(self, text, level=1):
+ """ Remove a tab from front of lines but allowing dedented lines. """
+ lines = text.split('\n')
+ for i in range(len(lines)):
+ if lines[i].startswith(' '*markdown.TAB_LENGTH*level):
+ lines[i] = lines[i][markdown.TAB_LENGTH*level:]
+ return '\n'.join(lines)
+
+ def test(self, parent, block):
+ """ Test for block type. Must be overridden by subclasses.
+
+ As the parser loops through processors, it will call the ``test`` method
+ on each to determine if the given block of text is of that type. This
+ method must return a boolean ``True`` or ``False``. The actual method of
+ testing is left to the needs of that particular block type. It could
+ be as simple as ``block.startswith(some_string)`` or a complex regular
+ expression. As the block type may be different depending on the parent
+ of the block (i.e. inside a list), the parent etree element is also
+ provided and may be used as part of the test.
+
+ Keywords:
+
+ * ``parent``: A etree element which will be the parent of the block.
+ * ``block``: A block of text from the source which has been split at
+ blank lines.
+ """
+ pass
+
+ def run(self, parent, blocks):
+ """ Run processor. Must be overridden by subclasses.
+
+ When the parser determines the appropriate type of a block, the parser
+ will call the corresponding processor's ``run`` method. This method
+ should parse the individual lines of the block and append them to
+ the etree.
+
+ Note that both the ``parent`` and ``etree`` keywords are pointers
+ to instances of the objects which should be edited in place. Each
+ processor must make changes to the existing objects as there is no
+ mechanism to return new/different objects to replace them.
+
+ This means that this method should be adding SubElements or adding text
+ to the parent, and should remove (``pop``) or add (``insert``) items to
+ the list of blocks.
+
+ Keywords:
+
+ * ``parent``: A etree element which is the parent of the current block.
+ * ``blocks``: A list of all remaining blocks of the document.
+ """
+ pass
+
+
+class ListIndentProcessor(BlockProcessor):
+ """ Process children of list items.
+
+ Example:
+ * a list item
+ process this part
+
+ or this part
+
+ """
+
+ INDENT_RE = re.compile(r'^(([ ]{%s})+)'% markdown.TAB_LENGTH)
+ ITEM_TYPES = ['li']
+ LIST_TYPES = ['ul', 'ol']
+
+ def test(self, parent, block):
+ return block.startswith(' '*markdown.TAB_LENGTH) and \
+ not self.parser.state.isstate('detabbed') and \
+ (parent.tag in self.ITEM_TYPES or \
+ (len(parent) and parent[-1] and \
+ (parent[-1].tag in self.LIST_TYPES)
+ )
+ )
+
+ def run(self, parent, blocks):
+ block = blocks.pop(0)
+ level, sibling = self.get_level(parent, block)
+ block = self.looseDetab(block, level)
+
+ self.parser.state.set('detabbed')
+ if parent.tag in self.ITEM_TYPES:
+ # The parent is already a li. Just parse the child block.
+ self.parser.parseBlocks(parent, [block])
+ elif sibling.tag in self.ITEM_TYPES:
+ # The sibling is a li. Use it as parent.
+ self.parser.parseBlocks(sibling, [block])
+ elif len(sibling) and sibling[-1].tag in self.ITEM_TYPES:
+ # The parent is a list (``ol`` or ``ul``) which has children.
+ # Assume the last child li is the parent of this block.
+ if sibling[-1].text:
+ # If the parent li has text, that text needs to be moved to a p
+ block = '%s\n\n%s' % (sibling[-1].text, block)
+ sibling[-1].text = ''
+ self.parser.parseChunk(sibling[-1], block)
+ else:
+ self.create_item(sibling, block)
+ self.parser.state.reset()
+
+ def create_item(self, parent, block):
+ """ Create a new li and parse the block with it as the parent. """
+ li = markdown.etree.SubElement(parent, 'li')
+ self.parser.parseBlocks(li, [block])
+
+ def get_level(self, parent, block):
+ """ Get level of indent based on list level. """
+ # Get indent level
+ m = self.INDENT_RE.match(block)
+ if m:
+ indent_level = len(m.group(1))/markdown.TAB_LENGTH
+ else:
+ indent_level = 0
+ if self.parser.state.isstate('list'):
+ # We're in a tightlist - so we already are at correct parent.
+ level = 1
+ else:
+ # We're in a looselist - so we need to find parent.
+ level = 0
+ # Step through children of tree to find matching indent level.
+ while indent_level > level:
+ child = self.lastChild(parent)
+ if child and (child.tag in self.LIST_TYPES or child.tag in self.ITEM_TYPES):
+ if child.tag in self.LIST_TYPES:
+ level += 1
+ parent = child
+ else:
+ # No more child levels. If we're short of indent_level,
+ # we have a code block. So we stop here.
+ break
+ return level, parent
+
+
+class CodeBlockProcessor(BlockProcessor):
+ """ Process code blocks. """
+
+ def test(self, parent, block):
+ return block.startswith(' '*markdown.TAB_LENGTH)
+
+ def run(self, parent, blocks):
+ sibling = self.lastChild(parent)
+ block = blocks.pop(0)
+ theRest = ''
+ if sibling and sibling.tag == "pre" and len(sibling) \
+ and sibling[0].tag == "code":
+ # The previous block was a code block. As blank lines do not start
+ # new code blocks, append this block to the previous, adding back
+ # linebreaks removed from the split into a list.
+ code = sibling[0]
+ block, theRest = self.detab(block)
+ code.text = markdown.AtomicString('%s\n%s\n' % (code.text, block.rstrip()))
+ else:
+ # This is a new codeblock. Create the elements and insert text.
+ pre = markdown.etree.SubElement(parent, 'pre')
+ code = markdown.etree.SubElement(pre, 'code')
+ block, theRest = self.detab(block)
+ code.text = markdown.AtomicString('%s\n' % block.rstrip())
+ if theRest:
+ # This block contained unindented line(s) after the first indented
+ # line. Insert these lines as the first block of the master blocks
+ # list for future processing.
+ blocks.insert(0, theRest)
+
+
+class BlockQuoteProcessor(BlockProcessor):
+
+ RE = re.compile(r'(^|\n)[ ]{0,3}>[ ]?(.*)')
+
+ def test(self, parent, block):
+ return bool(self.RE.search(block))
+
+ def run(self, parent, blocks):
+ block = blocks.pop(0)
+ m = self.RE.search(block)
+ if m:
+ before = block[:m.start()] # Lines before blockquote
+ # Pass lines before blockquote in recursively for parsing forst.
+ self.parser.parseBlocks(parent, [before])
+ # Remove ``> `` from begining of each line.
+ block = '\n'.join([self.clean(line) for line in
+ block[m.start():].split('\n')])
+ sibling = self.lastChild(parent)
+ if sibling and sibling.tag == "blockquote":
+ # Previous block was a blockquote so set that as this blocks parent
+ quote = sibling
+ else:
+ # This is a new blockquote. Create a new parent element.
+ quote = markdown.etree.SubElement(parent, 'blockquote')
+ # Recursively parse block with blockquote as parent.
+ self.parser.parseChunk(quote, block)
+
+ def clean(self, line):
+ """ Remove ``>`` from beginning of a line. """
+ m = self.RE.match(line)
+ if line.strip() == ">":
+ return ""
+ elif m:
+ return m.group(2)
+ else:
+ return line
+
+class OListProcessor(BlockProcessor):
+ """ Process ordered list blocks. """
+
+ TAG = 'ol'
+ # Detect an item (``1. item``). ``group(1)`` contains contents of item.
+ RE = re.compile(r'^[ ]{0,3}\d+\.[ ](.*)')
+ # Detect items on secondary lines. they can be of either list type.
+ CHILD_RE = re.compile(r'^[ ]{0,3}((\d+\.)|[*+-])[ ](.*)')
+ # Detect indented (nested) items of either type
+ INDENT_RE = re.compile(r'^[ ]{4,7}((\d+\.)|[*+-])[ ].*')
+
+ def test(self, parent, block):
+ return bool(self.RE.match(block))
+
+ def run(self, parent, blocks):
+ # Check fr multiple items in one block.
+ items = self.get_items(blocks.pop(0))
+ sibling = self.lastChild(parent)
+ if sibling and sibling.tag in ['ol', 'ul']:
+ # Previous block was a list item, so set that as parent
+ lst = sibling
+ # make sure previous item is in a p.
+ if len(lst) and lst[-1].text and not len(lst[-1]):
+ p = markdown.etree.SubElement(lst[-1], 'p')
+ p.text = lst[-1].text
+ lst[-1].text = ''
+ # parse first block differently as it gets wrapped in a p.
+ li = markdown.etree.SubElement(lst, 'li')
+ self.parser.state.set('looselist')
+ firstitem = items.pop(0)
+ self.parser.parseBlocks(li, [firstitem])
+ self.parser.state.reset()
+ else:
+ # This is a new list so create parent with appropriate tag.
+ lst = markdown.etree.SubElement(parent, self.TAG)
+ self.parser.state.set('list')
+ # Loop through items in block, recursively parsing each with the
+ # appropriate parent.
+ for item in items:
+ if item.startswith(' '*markdown.TAB_LENGTH):
+ # Item is indented. Parse with last item as parent
+ self.parser.parseBlocks(lst[-1], [item])
+ else:
+ # New item. Create li and parse with it as parent
+ li = markdown.etree.SubElement(lst, 'li')
+ self.parser.parseBlocks(li, [item])
+ self.parser.state.reset()
+
+ def get_items(self, block):
+ """ Break a block into list items. """
+ items = []
+ for line in block.split('\n'):
+ m = self.CHILD_RE.match(line)
+ if m:
+ # This is a new item. Append
+ items.append(m.group(3))
+ elif self.INDENT_RE.match(line):
+ # This is an indented (possibly nested) item.
+ if items[-1].startswith(' '*markdown.TAB_LENGTH):
+ # Previous item was indented. Append to that item.
+ items[-1] = '%s\n%s' % (items[-1], line)
+ else:
+ items.append(line)
+ else:
+ # This is another line of previous item. Append to that item.
+ items[-1] = '%s\n%s' % (items[-1], line)
+ return items
+
+
+class UListProcessor(OListProcessor):
+ """ Process unordered list blocks. """
+
+ TAG = 'ul'
+ RE = re.compile(r'^[ ]{0,3}[*+-][ ](.*)')
+
+
+class HashHeaderProcessor(BlockProcessor):
+ """ Process Hash Headers. """
+
+ # Detect a header at start of any line in block
+ RE = re.compile(r'(^|\n)(?P<level>#{1,6})(?P<header>.*?)#*(\n|$)')
+
+ def test(self, parent, block):
+ return bool(self.RE.search(block))
+
+ def run(self, parent, blocks):
+ block = blocks.pop(0)
+ m = self.RE.search(block)
+ if m:
+ before = block[:m.start()] # All lines before header
+ after = block[m.end():] # All lines after header
+ if before:
+ # As the header was not the first line of the block and the
+ # lines before the header must be parsed first,
+ # recursively parse this lines as a block.
+ self.parser.parseBlocks(parent, [before])
+ # Create header using named groups from RE
+ h = markdown.etree.SubElement(parent, 'h%d' % len(m.group('level')))
+ h.text = m.group('header').strip()
+ if after:
+ # Insert remaining lines as first block for future parsing.
+ blocks.insert(0, after)
+ else:
+ # This should never happen, but just in case...
+ message(CRITICAL, "We've got a problem header!")
+
+
+class SetextHeaderProcessor(BlockProcessor):
+ """ Process Setext-style Headers. """
+
+ # Detect Setext-style header. Must be first 2 lines of block.
+ RE = re.compile(r'^.*?\n[=-]{3,}', re.MULTILINE)
+
+ def test(self, parent, block):
+ return bool(self.RE.match(block))
+
+ def run(self, parent, blocks):
+ lines = blocks.pop(0).split('\n')
+ # Determine level. ``=`` is 1 and ``-`` is 2.
+ if lines[1].startswith('='):
+ level = 1
+ else:
+ level = 2
+ h = markdown.etree.SubElement(parent, 'h%d' % level)
+ h.text = lines[0].strip()
+ if len(lines) > 2:
+ # Block contains additional lines. Add to master blocks for later.
+ blocks.insert(0, '\n'.join(lines[2:]))
+
+
+class HRProcessor(BlockProcessor):
+ """ Process Horizontal Rules. """
+
+ RE = r'[ ]{0,3}(?P<ch>[*_-])[ ]?((?P=ch)[ ]?){2,}[ ]*'
+ # Detect hr on any line of a block.
+ SEARCH_RE = re.compile(r'(^|\n)%s(\n|$)' % RE)
+ # Match a hr on a single line of text.
+ MATCH_RE = re.compile(r'^%s$' % RE)
+
+ def test(self, parent, block):
+ return bool(self.SEARCH_RE.search(block))
+
+ def run(self, parent, blocks):
+ lines = blocks.pop(0).split('\n')
+ prelines = []
+ # Check for lines in block before hr.
+ for line in lines:
+ m = self.MATCH_RE.match(line)
+ if m:
+ break
+ else:
+ prelines.append(line)
+ if len(prelines):
+ # Recursively parse lines before hr so they get parsed first.
+ self.parser.parseBlocks(parent, ['\n'.join(prelines)])
+ # create hr
+ hr = markdown.etree.SubElement(parent, 'hr')
+ # check for lines in block after hr.
+ lines = lines[len(prelines)+1:]
+ if len(lines):
+ # Add lines after hr to master blocks for later parsing.
+ blocks.insert(0, '\n'.join(lines))
+
+
+class EmptyBlockProcessor(BlockProcessor):
+ """ Process blocks and start with an empty line. """
+
+ # Detect a block that only contains whitespace
+ # or only whitespace on the first line.
+ RE = re.compile(r'^\s*\n')
+
+ def test(self, parent, block):
+ return bool(self.RE.match(block))
+
+ def run(self, parent, blocks):
+ block = blocks.pop(0)
+ m = self.RE.match(block)
+ if m:
+ # Add remaining line to master blocks for later.
+ blocks.insert(0, block[m.end():])
+ sibling = self.lastChild(parent)
+ if sibling and sibling.tag == 'pre' and sibling[0] and \
+ sibling[0].tag == 'code':
+ # Last block is a codeblock. Append to preserve whitespace.
+ sibling[0].text = markdown.AtomicString('%s/n/n/n' % sibling[0].text )
+
+
+class ParagraphProcessor(BlockProcessor):
+ """ Process Paragraph blocks. """
+
+ def test(self, parent, block):
+ return True
+
+ def run(self, parent, blocks):
+ block = blocks.pop(0)
+ if block.strip():
+ # Not a blank block. Add to parent, otherwise throw it away.
+ if self.parser.state.isstate('list'):
+ # The parent is a tight-list. Append to parent.text
+ if parent.text:
+ parent.text = '%s\n%s' % (parent.text, block)
+ else:
+ parent.text = block.lstrip()
+ else:
+ # Create a regular paragraph
+ p = markdown.etree.SubElement(parent, 'p')
+ p.text = block.lstrip()
diff --git a/vendor/tornado/website/markdown/commandline.py b/vendor/tornado/website/markdown/commandline.py
new file mode 100644
index 0000000000..1eedc6dbb1
--- /dev/null
+++ b/vendor/tornado/website/markdown/commandline.py
@@ -0,0 +1,96 @@
+"""
+COMMAND-LINE SPECIFIC STUFF
+=============================================================================
+
+The rest of the code is specifically for handling the case where Python
+Markdown is called from the command line.
+"""
+
+import markdown
+import sys
+import logging
+from logging import DEBUG, INFO, WARN, ERROR, CRITICAL
+
+EXECUTABLE_NAME_FOR_USAGE = "python markdown.py"
+""" The name used in the usage statement displayed for python versions < 2.3.
+(With python 2.3 and higher the usage statement is generated by optparse
+and uses the actual name of the executable called.) """
+
+OPTPARSE_WARNING = """
+Python 2.3 or higher required for advanced command line options.
+For lower versions of Python use:
+
+ %s INPUT_FILE > OUTPUT_FILE
+
+""" % EXECUTABLE_NAME_FOR_USAGE
+
+def parse_options():
+ """
+ Define and parse `optparse` options for command-line usage.
+ """
+
+ try:
+ optparse = __import__("optparse")
+ except:
+ if len(sys.argv) == 2:
+ return {'input': sys.argv[1],
+ 'output': None,
+ 'safe': False,
+ 'extensions': [],
+ 'encoding': None }, CRITICAL
+ else:
+ print OPTPARSE_WARNING
+ return None, None
+
+ parser = optparse.OptionParser(usage="%prog INPUTFILE [options]")
+ parser.add_option("-f", "--file", dest="filename", default=sys.stdout,
+ help="write output to OUTPUT_FILE",
+ metavar="OUTPUT_FILE")
+ parser.add_option("-e", "--encoding", dest="encoding",
+ help="encoding for input and output files",)
+ parser.add_option("-q", "--quiet", default = CRITICAL,
+ action="store_const", const=CRITICAL+10, dest="verbose",
+ help="suppress all messages")
+ parser.add_option("-v", "--verbose",
+ action="store_const", const=INFO, dest="verbose",
+ help="print info messages")
+ parser.add_option("-s", "--safe", dest="safe", default=False,
+ metavar="SAFE_MODE",
+ help="safe mode ('replace', 'remove' or 'escape' user's HTML tag)")
+ parser.add_option("-o", "--output_format", dest="output_format",
+ default='xhtml1', metavar="OUTPUT_FORMAT",
+ help="Format of output. One of 'xhtml1' (default) or 'html4'.")
+ parser.add_option("--noisy",
+ action="store_const", const=DEBUG, dest="verbose",
+ help="print debug messages")
+ parser.add_option("-x", "--extension", action="append", dest="extensions",
+ help = "load extension EXTENSION", metavar="EXTENSION")
+
+ (options, args) = parser.parse_args()
+
+ if not len(args) == 1:
+ parser.print_help()
+ return None, None
+ else:
+ input_file = args[0]
+
+ if not options.extensions:
+ options.extensions = []
+
+ return {'input': input_file,
+ 'output': options.filename,
+ 'safe_mode': options.safe,
+ 'extensions': options.extensions,
+ 'encoding': options.encoding,
+ 'output_format': options.output_format}, options.verbose
+
+def run():
+ """Run Markdown from the command line."""
+
+ # Parse options and adjust logging level if necessary
+ options, logging_level = parse_options()
+ if not options: sys.exit(0)
+ if logging_level: logging.getLogger('MARKDOWN').setLevel(logging_level)
+
+ # Run
+ markdown.markdownFromFile(**options)
diff --git a/vendor/tornado/website/markdown/etree_loader.py b/vendor/tornado/website/markdown/etree_loader.py
new file mode 100644
index 0000000000..e2599b2cb9
--- /dev/null
+++ b/vendor/tornado/website/markdown/etree_loader.py
@@ -0,0 +1,33 @@
+
+from markdown import message, CRITICAL
+import sys
+
+## Import
+def importETree():
+ """Import the best implementation of ElementTree, return a module object."""
+ etree_in_c = None
+ try: # Is it Python 2.5+ with C implemenation of ElementTree installed?
+ import xml.etree.cElementTree as etree_in_c
+ except ImportError:
+ try: # Is it Python 2.5+ with Python implementation of ElementTree?
+ import xml.etree.ElementTree as etree
+ except ImportError:
+ try: # An earlier version of Python with cElementTree installed?
+ import cElementTree as etree_in_c
+ except ImportError:
+ try: # An earlier version of Python with Python ElementTree?
+ import elementtree.ElementTree as etree
+ except ImportError:
+ message(CRITICAL, "Failed to import ElementTree")
+ sys.exit(1)
+ if etree_in_c and etree_in_c.VERSION < "1.0":
+ message(CRITICAL, "For cElementTree version 1.0 or higher is required.")
+ sys.exit(1)
+ elif etree_in_c :
+ return etree_in_c
+ elif etree.VERSION < "1.1":
+ message(CRITICAL, "For ElementTree version 1.1 or higher is required")
+ sys.exit(1)
+ else :
+ return etree
+
diff --git a/vendor/tornado/website/markdown/extensions/__init__.py b/vendor/tornado/website/markdown/extensions/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/vendor/tornado/website/markdown/extensions/__init__.py
diff --git a/vendor/tornado/website/markdown/extensions/toc.py b/vendor/tornado/website/markdown/extensions/toc.py
new file mode 100644
index 0000000000..3afaea0488
--- /dev/null
+++ b/vendor/tornado/website/markdown/extensions/toc.py
@@ -0,0 +1,140 @@
+"""
+Table of Contents Extension for Python-Markdown
+* * *
+
+(c) 2008 [Jack Miller](http://codezen.org)
+
+Dependencies:
+* [Markdown 2.0+](http://www.freewisdom.org/projects/python-markdown/)
+
+"""
+import markdown
+from markdown import etree
+import re
+
+class TocTreeprocessor(markdown.treeprocessors.Treeprocessor):
+ # Iterator wrapper to get parent and child all at once
+ def iterparent(self, root):
+ for parent in root.getiterator():
+ for child in parent:
+ yield parent, child
+
+ def run(self, doc):
+ div = etree.Element("div")
+ div.attrib["class"] = "toc"
+ last_li = None
+
+ # Add title to the div
+ if self.config["title"][0]:
+ header = etree.SubElement(div, "span")
+ header.attrib["class"] = "toctitle"
+ header.text = self.config["title"][0]
+
+ level = 0
+ list_stack=[div]
+ header_rgx = re.compile("[Hh][123456]")
+
+ # Get a list of id attributes
+ used_ids = []
+ for c in doc.getiterator():
+ if "id" in c.attrib:
+ used_ids.append(c.attrib["id"])
+
+ for (p, c) in self.iterparent(doc):
+ if not c.text:
+ continue
+
+ # To keep the output from screwing up the
+ # validation by putting a <div> inside of a <p>
+ # we actually replace the <p> in its entirety.
+ # We do not allow the marker inside a header as that
+ # would causes an enless loop of placing a new TOC
+ # inside previously generated TOC.
+
+ if c.text.find(self.config["marker"][0]) > -1 and not header_rgx.match(c.tag):
+ for i in range(len(p)):
+ if p[i] == c:
+ p[i] = div
+ break
+
+ if header_rgx.match(c.tag):
+ tag_level = int(c.tag[-1])
+
+ # Regardless of how many levels we jumped
+ # only one list should be created, since
+ # empty lists containing lists are illegal.
+
+ if tag_level < level:
+ list_stack.pop()
+ level = tag_level
+
+ if tag_level > level:
+ newlist = etree.Element("ul")
+ if last_li:
+ last_li.append(newlist)
+ else:
+ list_stack[-1].append(newlist)
+ list_stack.append(newlist)
+ level = tag_level
+
+ # Do not override pre-existing ids
+ if not "id" in c.attrib:
+ id = self.config["slugify"][0](c.text)
+ if id in used_ids:
+ ctr = 1
+ while "%s_%d" % (id, ctr) in used_ids:
+ ctr += 1
+ id = "%s_%d" % (id, ctr)
+ used_ids.append(id)
+ c.attrib["id"] = id
+ else:
+ id = c.attrib["id"]
+
+ # List item link, to be inserted into the toc div
+ last_li = etree.Element("li")
+ link = etree.SubElement(last_li, "a")
+ link.text = c.text
+ link.attrib["href"] = '#' + id
+
+ if int(self.config["anchorlink"][0]):
+ anchor = etree.SubElement(c, "a")
+ anchor.text = c.text
+ anchor.attrib["href"] = "#" + id
+ anchor.attrib["class"] = "toclink"
+ c.text = ""
+
+ list_stack[-1].append(last_li)
+
+class TocExtension(markdown.Extension):
+ def __init__(self, configs):
+ self.config = { "marker" : ["[TOC]",
+ "Text to find and replace with Table of Contents -"
+ "Defaults to \"[TOC]\""],
+ "slugify" : [self.slugify,
+ "Function to generate anchors based on header text-"
+ "Defaults to a built in slugify function."],
+ "title" : [None,
+ "Title to insert into TOC <div> - "
+ "Defaults to None"],
+ "anchorlink" : [0,
+ "1 if header should be a self link"
+ "Defaults to 0"]}
+
+ for key, value in configs:
+ self.setConfig(key, value)
+
+ # This is exactly the same as Django's slugify
+ def slugify(self, value):
+ """ Slugify a string, to make it URL friendly. """
+ import unicodedata
+ value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore')
+ value = unicode(re.sub('[^\w\s-]', '', value).strip().lower())
+ return re.sub('[-\s]+','-',value)
+
+ def extendMarkdown(self, md, md_globals):
+ tocext = TocTreeprocessor(md)
+ tocext.config = self.config
+ md.treeprocessors.add("toc", tocext, "_begin")
+
+def makeExtension(configs={}):
+ return TocExtension(configs=configs)
diff --git a/vendor/tornado/website/markdown/html4.py b/vendor/tornado/website/markdown/html4.py
new file mode 100644
index 0000000000..08f241d57a
--- /dev/null
+++ b/vendor/tornado/website/markdown/html4.py
@@ -0,0 +1,274 @@
+# markdown/html4.py
+#
+# Add html4 serialization to older versions of Elementree
+# Taken from ElementTree 1.3 preview with slight modifications
+#
+# Copyright (c) 1999-2007 by Fredrik Lundh. All rights reserved.
+#
+# fredrik@pythonware.com
+# http://www.pythonware.com
+#
+# --------------------------------------------------------------------
+# The ElementTree toolkit is
+#
+# Copyright (c) 1999-2007 by Fredrik Lundh
+#
+# By obtaining, using, and/or copying this software and/or its
+# associated documentation, you agree that you have read, understood,
+# and will comply with the following terms and conditions:
+#
+# Permission to use, copy, modify, and distribute this software and
+# its associated documentation for any purpose and without fee is
+# hereby granted, provided that the above copyright notice appears in
+# all copies, and that both that copyright notice and this permission
+# notice appear in supporting documentation, and that the name of
+# Secret Labs AB or the author not be used in advertising or publicity
+# pertaining to distribution of the software without specific, written
+# prior permission.
+#
+# SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD
+# TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANT-
+# ABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR
+# BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY
+# DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
+# WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS
+# ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE
+# OF THIS SOFTWARE.
+# --------------------------------------------------------------------
+
+
+import markdown
+ElementTree = markdown.etree.ElementTree
+QName = markdown.etree.QName
+Comment = markdown.etree.Comment
+PI = markdown.etree.PI
+ProcessingInstruction = markdown.etree.ProcessingInstruction
+
+HTML_EMPTY = ("area", "base", "basefont", "br", "col", "frame", "hr",
+ "img", "input", "isindex", "link", "meta" "param")
+
+try:
+ HTML_EMPTY = set(HTML_EMPTY)
+except NameError:
+ pass
+
+_namespace_map = {
+ # "well-known" namespace prefixes
+ "http://www.w3.org/XML/1998/namespace": "xml",
+ "http://www.w3.org/1999/xhtml": "html",
+ "http://www.w3.org/1999/02/22-rdf-syntax-ns#": "rdf",
+ "http://schemas.xmlsoap.org/wsdl/": "wsdl",
+ # xml schema
+ "http://www.w3.org/2001/XMLSchema": "xs",
+ "http://www.w3.org/2001/XMLSchema-instance": "xsi",
+ # dublic core
+ "http://purl.org/dc/elements/1.1/": "dc",
+}
+
+
+def _raise_serialization_error(text):
+ raise TypeError(
+ "cannot serialize %r (type %s)" % (text, type(text).__name__)
+ )
+
+def _encode(text, encoding):
+ try:
+ return text.encode(encoding, "xmlcharrefreplace")
+ except (TypeError, AttributeError):
+ _raise_serialization_error(text)
+
+def _escape_cdata(text, encoding):
+ # escape character data
+ try:
+ # it's worth avoiding do-nothing calls for strings that are
+ # shorter than 500 character, or so. assume that's, by far,
+ # the most common case in most applications.
+ if "&" in text:
+ text = text.replace("&", "&amp;")
+ if "<" in text:
+ text = text.replace("<", "&lt;")
+ if ">" in text:
+ text = text.replace(">", "&gt;")
+ return text.encode(encoding, "xmlcharrefreplace")
+ except (TypeError, AttributeError):
+ _raise_serialization_error(text)
+
+
+def _escape_attrib(text, encoding):
+ # escape attribute value
+ try:
+ if "&" in text:
+ text = text.replace("&", "&amp;")
+ if "<" in text:
+ text = text.replace("<", "&lt;")
+ if ">" in text:
+ text = text.replace(">", "&gt;")
+ if "\"" in text:
+ text = text.replace("\"", "&quot;")
+ if "\n" in text:
+ text = text.replace("\n", "&#10;")
+ return text.encode(encoding, "xmlcharrefreplace")
+ except (TypeError, AttributeError):
+ _raise_serialization_error(text)
+
+def _escape_attrib_html(text, encoding):
+ # escape attribute value
+ try:
+ if "&" in text:
+ text = text.replace("&", "&amp;")
+ if ">" in text:
+ text = text.replace(">", "&gt;")
+ if "\"" in text:
+ text = text.replace("\"", "&quot;")
+ return text.encode(encoding, "xmlcharrefreplace")
+ except (TypeError, AttributeError):
+ _raise_serialization_error(text)
+
+
+def _serialize_html(write, elem, encoding, qnames, namespaces):
+ tag = elem.tag
+ text = elem.text
+ if tag is Comment:
+ write("<!--%s-->" % _escape_cdata(text, encoding))
+ elif tag is ProcessingInstruction:
+ write("<?%s?>" % _escape_cdata(text, encoding))
+ else:
+ tag = qnames[tag]
+ if tag is None:
+ if text:
+ write(_escape_cdata(text, encoding))
+ for e in elem:
+ _serialize_html(write, e, encoding, qnames, None)
+ else:
+ write("<" + tag)
+ items = elem.items()
+ if items or namespaces:
+ items.sort() # lexical order
+ for k, v in items:
+ if isinstance(k, QName):
+ k = k.text
+ if isinstance(v, QName):
+ v = qnames[v.text]
+ else:
+ v = _escape_attrib_html(v, encoding)
+ # FIXME: handle boolean attributes
+ write(" %s=\"%s\"" % (qnames[k], v))
+ if namespaces:
+ items = namespaces.items()
+ items.sort(key=lambda x: x[1]) # sort on prefix
+ for v, k in items:
+ if k:
+ k = ":" + k
+ write(" xmlns%s=\"%s\"" % (
+ k.encode(encoding),
+ _escape_attrib(v, encoding)
+ ))
+ write(">")
+ tag = tag.lower()
+ if text:
+ if tag == "script" or tag == "style":
+ write(_encode(text, encoding))
+ else:
+ write(_escape_cdata(text, encoding))
+ for e in elem:
+ _serialize_html(write, e, encoding, qnames, None)
+ if tag not in HTML_EMPTY:
+ write("</" + tag + ">")
+ if elem.tail:
+ write(_escape_cdata(elem.tail, encoding))
+
+def write_html(root, f,
+ # keyword arguments
+ encoding="us-ascii",
+ default_namespace=None):
+ assert root is not None
+ if not hasattr(f, "write"):
+ f = open(f, "wb")
+ write = f.write
+ if not encoding:
+ encoding = "us-ascii"
+ qnames, namespaces = _namespaces(
+ root, encoding, default_namespace
+ )
+ _serialize_html(
+ write, root, encoding, qnames, namespaces
+ )
+
+# --------------------------------------------------------------------
+# serialization support
+
+def _namespaces(elem, encoding, default_namespace=None):
+ # identify namespaces used in this tree
+
+ # maps qnames to *encoded* prefix:local names
+ qnames = {None: None}
+
+ # maps uri:s to prefixes
+ namespaces = {}
+ if default_namespace:
+ namespaces[default_namespace] = ""
+
+ def encode(text):
+ return text.encode(encoding)
+
+ def add_qname(qname):
+ # calculate serialized qname representation
+ try:
+ if qname[:1] == "{":
+ uri, tag = qname[1:].split("}", 1)
+ prefix = namespaces.get(uri)
+ if prefix is None:
+ prefix = _namespace_map.get(uri)
+ if prefix is None:
+ prefix = "ns%d" % len(namespaces)
+ if prefix != "xml":
+ namespaces[uri] = prefix
+ if prefix:
+ qnames[qname] = encode("%s:%s" % (prefix, tag))
+ else:
+ qnames[qname] = encode(tag) # default element
+ else:
+ if default_namespace:
+ # FIXME: can this be handled in XML 1.0?
+ raise ValueError(
+ "cannot use non-qualified names with "
+ "default_namespace option"
+ )
+ qnames[qname] = encode(qname)
+ except TypeError:
+ _raise_serialization_error(qname)
+
+ # populate qname and namespaces table
+ try:
+ iterate = elem.iter
+ except AttributeError:
+ iterate = elem.getiterator # cET compatibility
+ for elem in iterate():
+ tag = elem.tag
+ if isinstance(tag, QName) and tag.text not in qnames:
+ add_qname(tag.text)
+ elif isinstance(tag, basestring):
+ if tag not in qnames:
+ add_qname(tag)
+ elif tag is not None and tag is not Comment and tag is not PI:
+ _raise_serialization_error(tag)
+ for key, value in elem.items():
+ if isinstance(key, QName):
+ key = key.text
+ if key not in qnames:
+ add_qname(key)
+ if isinstance(value, QName) and value.text not in qnames:
+ add_qname(value.text)
+ text = elem.text
+ if isinstance(text, QName) and text.text not in qnames:
+ add_qname(text.text)
+ return qnames, namespaces
+
+def to_html_string(element, encoding=None):
+ class dummy:
+ pass
+ data = []
+ file = dummy()
+ file.write = data.append
+ write_html(ElementTree(element).getroot(),file,encoding)
+ return "".join(data)
diff --git a/vendor/tornado/website/markdown/inlinepatterns.py b/vendor/tornado/website/markdown/inlinepatterns.py
new file mode 100644
index 0000000000..89fa3b2ef4
--- /dev/null
+++ b/vendor/tornado/website/markdown/inlinepatterns.py
@@ -0,0 +1,371 @@
+"""
+INLINE PATTERNS
+=============================================================================
+
+Inline patterns such as *emphasis* are handled by means of auxiliary
+objects, one per pattern. Pattern objects must be instances of classes
+that extend markdown.Pattern. Each pattern object uses a single regular
+expression and needs support the following methods:
+
+ pattern.getCompiledRegExp() # returns a regular expression
+
+ pattern.handleMatch(m) # takes a match object and returns
+ # an ElementTree element or just plain text
+
+All of python markdown's built-in patterns subclass from Pattern,
+but you can add additional patterns that don't.
+
+Also note that all the regular expressions used by inline must
+capture the whole block. For this reason, they all start with
+'^(.*)' and end with '(.*)!'. In case with built-in expression
+Pattern takes care of adding the "^(.*)" and "(.*)!".
+
+Finally, the order in which regular expressions are applied is very
+important - e.g. if we first replace http://.../ links with <a> tags
+and _then_ try to replace inline html, we would end up with a mess.
+So, we apply the expressions in the following order:
+
+* escape and backticks have to go before everything else, so
+ that we can preempt any markdown patterns by escaping them.
+
+* then we handle auto-links (must be done before inline html)
+
+* then we handle inline HTML. At this point we will simply
+ replace all inline HTML strings with a placeholder and add
+ the actual HTML to a hash.
+
+* then inline images (must be done before links)
+
+* then bracketed links, first regular then reference-style
+
+* finally we apply strong and emphasis
+"""
+
+import markdown
+import re
+from urlparse import urlparse, urlunparse
+import sys
+if sys.version >= "3.0":
+ from html import entities as htmlentitydefs
+else:
+ import htmlentitydefs
+
+"""
+The actual regular expressions for patterns
+-----------------------------------------------------------------------------
+"""
+
+NOBRACKET = r'[^\]\[]*'
+BRK = ( r'\[('
+ + (NOBRACKET + r'(\[')*6
+ + (NOBRACKET+ r'\])*')*6
+ + NOBRACKET + r')\]' )
+NOIMG = r'(?<!\!)'
+
+BACKTICK_RE = r'(?<!\\)(`+)(.+?)(?<!`)\2(?!`)' # `e=f()` or ``e=f("`")``
+ESCAPE_RE = r'\\(.)' # \<
+EMPHASIS_RE = r'(\*)([^\*]*)\2' # *emphasis*
+STRONG_RE = r'(\*{2}|_{2})(.*?)\2' # **strong**
+STRONG_EM_RE = r'(\*{3}|_{3})(.*?)\2' # ***strong***
+
+if markdown.SMART_EMPHASIS:
+ EMPHASIS_2_RE = r'(?<!\S)(_)(\S.*?)\2' # _emphasis_
+else:
+ EMPHASIS_2_RE = r'(_)(.*?)\2' # _emphasis_
+
+LINK_RE = NOIMG + BRK + \
+r'''\(\s*(<.*?>|((?:(?:\(.*?\))|[^\(\)]))*?)\s*((['"])(.*)\12)?\)'''
+# [text](url) or [text](<url>)
+
+IMAGE_LINK_RE = r'\!' + BRK + r'\s*\((<.*?>|([^\)]*))\)'
+# ![alttxt](http://x.com/) or ![alttxt](<http://x.com/>)
+REFERENCE_RE = NOIMG + BRK+ r'\s*\[([^\]]*)\]' # [Google][3]
+IMAGE_REFERENCE_RE = r'\!' + BRK + '\s*\[([^\]]*)\]' # ![alt text][2]
+NOT_STRONG_RE = r'( \* )' # stand-alone * or _
+AUTOLINK_RE = r'<((?:f|ht)tps?://[^>]*)>' # <http://www.123.com>
+AUTOMAIL_RE = r'<([^> \!]*@[^> ]*)>' # <me@example.com>
+
+HTML_RE = r'(\<([a-zA-Z/][^\>]*?|\!--.*?--)\>)' # <...>
+ENTITY_RE = r'(&[\#a-zA-Z0-9]*;)' # &amp;
+LINE_BREAK_RE = r' \n' # two spaces at end of line
+LINE_BREAK_2_RE = r' $' # two spaces at end of text
+
+
+def dequote(string):
+ """Remove quotes from around a string."""
+ if ( ( string.startswith('"') and string.endswith('"'))
+ or (string.startswith("'") and string.endswith("'")) ):
+ return string[1:-1]
+ else:
+ return string
+
+ATTR_RE = re.compile("\{@([^\}]*)=([^\}]*)}") # {@id=123}
+
+def handleAttributes(text, parent):
+ """Set values of an element based on attribute definitions ({@id=123})."""
+ def attributeCallback(match):
+ parent.set(match.group(1), match.group(2).replace('\n', ' '))
+ return ATTR_RE.sub(attributeCallback, text)
+
+
+"""
+The pattern classes
+-----------------------------------------------------------------------------
+"""
+
+class Pattern:
+ """Base class that inline patterns subclass. """
+
+ def __init__ (self, pattern, markdown_instance=None):
+ """
+ Create an instant of an inline pattern.
+
+ Keyword arguments:
+
+ * pattern: A regular expression that matches a pattern
+
+ """
+ self.pattern = pattern
+ self.compiled_re = re.compile("^(.*?)%s(.*?)$" % pattern, re.DOTALL)
+
+ # Api for Markdown to pass safe_mode into instance
+ self.safe_mode = False
+ if markdown_instance:
+ self.markdown = markdown_instance
+
+ def getCompiledRegExp (self):
+ """ Return a compiled regular expression. """
+ return self.compiled_re
+
+ def handleMatch(self, m):
+ """Return a ElementTree element from the given match.
+
+ Subclasses should override this method.
+
+ Keyword arguments:
+
+ * m: A re match object containing a match of the pattern.
+
+ """
+ pass
+
+ def type(self):
+ """ Return class name, to define pattern type """
+ return self.__class__.__name__
+
+BasePattern = Pattern # for backward compatibility
+
+class SimpleTextPattern (Pattern):
+ """ Return a simple text of group(2) of a Pattern. """
+ def handleMatch(self, m):
+ text = m.group(2)
+ if text == markdown.INLINE_PLACEHOLDER_PREFIX:
+ return None
+ return text
+
+class SimpleTagPattern (Pattern):
+ """
+ Return element of type `tag` with a text attribute of group(3)
+ of a Pattern.
+
+ """
+ def __init__ (self, pattern, tag):
+ Pattern.__init__(self, pattern)
+ self.tag = tag
+
+ def handleMatch(self, m):
+ el = markdown.etree.Element(self.tag)
+ el.text = m.group(3)
+ return el
+
+
+class SubstituteTagPattern (SimpleTagPattern):
+ """ Return a eLement of type `tag` with no children. """
+ def handleMatch (self, m):
+ return markdown.etree.Element(self.tag)
+
+
+class BacktickPattern (Pattern):
+ """ Return a `<code>` element containing the matching text. """
+ def __init__ (self, pattern):
+ Pattern.__init__(self, pattern)
+ self.tag = "code"
+
+ def handleMatch(self, m):
+ el = markdown.etree.Element(self.tag)
+ el.text = markdown.AtomicString(m.group(3).strip())
+ return el
+
+
+class DoubleTagPattern (SimpleTagPattern):
+ """Return a ElementTree element nested in tag2 nested in tag1.
+
+ Useful for strong emphasis etc.
+
+ """
+ def handleMatch(self, m):
+ tag1, tag2 = self.tag.split(",")
+ el1 = markdown.etree.Element(tag1)
+ el2 = markdown.etree.SubElement(el1, tag2)
+ el2.text = m.group(3)
+ return el1
+
+
+class HtmlPattern (Pattern):
+ """ Store raw inline html and return a placeholder. """
+ def handleMatch (self, m):
+ rawhtml = m.group(2)
+ inline = True
+ place_holder = self.markdown.htmlStash.store(rawhtml)
+ return place_holder
+
+
+class LinkPattern (Pattern):
+ """ Return a link element from the given match. """
+ def handleMatch(self, m):
+ el = markdown.etree.Element("a")
+ el.text = m.group(2)
+ title = m.group(11)
+ href = m.group(9)
+
+ if href:
+ if href[0] == "<":
+ href = href[1:-1]
+ el.set("href", self.sanitize_url(href.strip()))
+ else:
+ el.set("href", "")
+
+ if title:
+ title = dequote(title) #.replace('"', "&quot;")
+ el.set("title", title)
+ return el
+
+ def sanitize_url(self, url):
+ """
+ Sanitize a url against xss attacks in "safe_mode".
+
+ Rather than specifically blacklisting `javascript:alert("XSS")` and all
+ its aliases (see <http://ha.ckers.org/xss.html>), we whitelist known
+ safe url formats. Most urls contain a network location, however some
+ are known not to (i.e.: mailto links). Script urls do not contain a
+ location. Additionally, for `javascript:...`, the scheme would be
+ "javascript" but some aliases will appear to `urlparse()` to have no
+ scheme. On top of that relative links (i.e.: "foo/bar.html") have no
+ scheme. Therefore we must check "path", "parameters", "query" and
+ "fragment" for any literal colons. We don't check "scheme" for colons
+ because it *should* never have any and "netloc" must allow the form:
+ `username:password@host:port`.
+
+ """
+ locless_schemes = ['', 'mailto', 'news']
+ scheme, netloc, path, params, query, fragment = url = urlparse(url)
+ safe_url = False
+ if netloc != '' or scheme in locless_schemes:
+ safe_url = True
+
+ for part in url[2:]:
+ if ":" in part:
+ safe_url = False
+
+ if self.markdown.safeMode and not safe_url:
+ return ''
+ else:
+ return urlunparse(url)
+
+class ImagePattern(LinkPattern):
+ """ Return a img element from the given match. """
+ def handleMatch(self, m):
+ el = markdown.etree.Element("img")
+ src_parts = m.group(9).split()
+ if src_parts:
+ src = src_parts[0]
+ if src[0] == "<" and src[-1] == ">":
+ src = src[1:-1]
+ el.set('src', self.sanitize_url(src))
+ else:
+ el.set('src', "")
+ if len(src_parts) > 1:
+ el.set('title', dequote(" ".join(src_parts[1:])))
+
+ if markdown.ENABLE_ATTRIBUTES:
+ truealt = handleAttributes(m.group(2), el)
+ else:
+ truealt = m.group(2)
+
+ el.set('alt', truealt)
+ return el
+
+class ReferencePattern(LinkPattern):
+ """ Match to a stored reference and return link element. """
+ def handleMatch(self, m):
+ if m.group(9):
+ id = m.group(9).lower()
+ else:
+ # if we got something like "[Google][]"
+ # we'll use "google" as the id
+ id = m.group(2).lower()
+
+ if not id in self.markdown.references: # ignore undefined refs
+ return None
+ href, title = self.markdown.references[id]
+
+ text = m.group(2)
+ return self.makeTag(href, title, text)
+
+ def makeTag(self, href, title, text):
+ el = markdown.etree.Element('a')
+
+ el.set('href', self.sanitize_url(href))
+ if title:
+ el.set('title', title)
+
+ el.text = text
+ return el
+
+
+class ImageReferencePattern (ReferencePattern):
+ """ Match to a stored reference and return img element. """
+ def makeTag(self, href, title, text):
+ el = markdown.etree.Element("img")
+ el.set("src", self.sanitize_url(href))
+ if title:
+ el.set("title", title)
+ el.set("alt", text)
+ return el
+
+
+class AutolinkPattern (Pattern):
+ """ Return a link Element given an autolink (`<http://example/com>`). """
+ def handleMatch(self, m):
+ el = markdown.etree.Element("a")
+ el.set('href', m.group(2))
+ el.text = markdown.AtomicString(m.group(2))
+ return el
+
+class AutomailPattern (Pattern):
+ """
+ Return a mailto link Element given an automail link (`<foo@example.com>`).
+ """
+ def handleMatch(self, m):
+ el = markdown.etree.Element('a')
+ email = m.group(2)
+ if email.startswith("mailto:"):
+ email = email[len("mailto:"):]
+
+ def codepoint2name(code):
+ """Return entity definition by code, or the code if not defined."""
+ entity = htmlentitydefs.codepoint2name.get(code)
+ if entity:
+ return "%s%s;" % (markdown.AMP_SUBSTITUTE, entity)
+ else:
+ return "%s#%d;" % (markdown.AMP_SUBSTITUTE, code)
+
+ letters = [codepoint2name(ord(letter)) for letter in email]
+ el.text = markdown.AtomicString(''.join(letters))
+
+ mailto = "mailto:" + email
+ mailto = "".join([markdown.AMP_SUBSTITUTE + '#%d;' %
+ ord(letter) for letter in mailto])
+ el.set('href', mailto)
+ return el
+
diff --git a/vendor/tornado/website/markdown/odict.py b/vendor/tornado/website/markdown/odict.py
new file mode 100644
index 0000000000..bf3ef07182
--- /dev/null
+++ b/vendor/tornado/website/markdown/odict.py
@@ -0,0 +1,162 @@
+class OrderedDict(dict):
+ """
+ A dictionary that keeps its keys in the order in which they're inserted.
+
+ Copied from Django's SortedDict with some modifications.
+
+ """
+ def __new__(cls, *args, **kwargs):
+ instance = super(OrderedDict, cls).__new__(cls, *args, **kwargs)
+ instance.keyOrder = []
+ return instance
+
+ def __init__(self, data=None):
+ if data is None:
+ data = {}
+ super(OrderedDict, self).__init__(data)
+ if isinstance(data, dict):
+ self.keyOrder = data.keys()
+ else:
+ self.keyOrder = []
+ for key, value in data:
+ if key not in self.keyOrder:
+ self.keyOrder.append(key)
+
+ def __deepcopy__(self, memo):
+ from copy import deepcopy
+ return self.__class__([(key, deepcopy(value, memo))
+ for key, value in self.iteritems()])
+
+ def __setitem__(self, key, value):
+ super(OrderedDict, self).__setitem__(key, value)
+ if key not in self.keyOrder:
+ self.keyOrder.append(key)
+
+ def __delitem__(self, key):
+ super(OrderedDict, self).__delitem__(key)
+ self.keyOrder.remove(key)
+
+ def __iter__(self):
+ for k in self.keyOrder:
+ yield k
+
+ def pop(self, k, *args):
+ result = super(OrderedDict, self).pop(k, *args)
+ try:
+ self.keyOrder.remove(k)
+ except ValueError:
+ # Key wasn't in the dictionary in the first place. No problem.
+ pass
+ return result
+
+ def popitem(self):
+ result = super(OrderedDict, self).popitem()
+ self.keyOrder.remove(result[0])
+ return result
+
+ def items(self):
+ return zip(self.keyOrder, self.values())
+
+ def iteritems(self):
+ for key in self.keyOrder:
+ yield key, super(OrderedDict, self).__getitem__(key)
+
+ def keys(self):
+ return self.keyOrder[:]
+
+ def iterkeys(self):
+ return iter(self.keyOrder)
+
+ def values(self):
+ return [super(OrderedDict, self).__getitem__(k) for k in self.keyOrder]
+
+ def itervalues(self):
+ for key in self.keyOrder:
+ yield super(OrderedDict, self).__getitem__(key)
+
+ def update(self, dict_):
+ for k, v in dict_.items():
+ self.__setitem__(k, v)
+
+ def setdefault(self, key, default):
+ if key not in self.keyOrder:
+ self.keyOrder.append(key)
+ return super(OrderedDict, self).setdefault(key, default)
+
+ def value_for_index(self, index):
+ """Return the value of the item at the given zero-based index."""
+ return self[self.keyOrder[index]]
+
+ def insert(self, index, key, value):
+ """Insert the key, value pair before the item with the given index."""
+ if key in self.keyOrder:
+ n = self.keyOrder.index(key)
+ del self.keyOrder[n]
+ if n < index:
+ index -= 1
+ self.keyOrder.insert(index, key)
+ super(OrderedDict, self).__setitem__(key, value)
+
+ def copy(self):
+ """Return a copy of this object."""
+ # This way of initializing the copy means it works for subclasses, too.
+ obj = self.__class__(self)
+ obj.keyOrder = self.keyOrder[:]
+ return obj
+
+ def __repr__(self):
+ """
+ Replace the normal dict.__repr__ with a version that returns the keys
+ in their sorted order.
+ """
+ return '{%s}' % ', '.join(['%r: %r' % (k, v) for k, v in self.items()])
+
+ def clear(self):
+ super(OrderedDict, self).clear()
+ self.keyOrder = []
+
+ def index(self, key):
+ """ Return the index of a given key. """
+ return self.keyOrder.index(key)
+
+ def index_for_location(self, location):
+ """ Return index or None for a given location. """
+ if location == '_begin':
+ i = 0
+ elif location == '_end':
+ i = None
+ elif location.startswith('<') or location.startswith('>'):
+ i = self.index(location[1:])
+ if location.startswith('>'):
+ if i >= len(self):
+ # last item
+ i = None
+ else:
+ i += 1
+ else:
+ raise ValueError('Not a valid location: "%s". Location key '
+ 'must start with a ">" or "<".' % location)
+ return i
+
+ def add(self, key, value, location):
+ """ Insert by key location. """
+ i = self.index_for_location(location)
+ if i is not None:
+ self.insert(i, key, value)
+ else:
+ self.__setitem__(key, value)
+
+ def link(self, key, location):
+ """ Change location of an existing item. """
+ n = self.keyOrder.index(key)
+ del self.keyOrder[n]
+ i = self.index_for_location(location)
+ try:
+ if i is not None:
+ self.keyOrder.insert(i, key)
+ else:
+ self.keyOrder.append(key)
+ except Error:
+ # restore to prevent data loss and reraise
+ self.keyOrder.insert(n, key)
+ raise Error
diff --git a/vendor/tornado/website/markdown/postprocessors.py b/vendor/tornado/website/markdown/postprocessors.py
new file mode 100644
index 0000000000..80227bb909
--- /dev/null
+++ b/vendor/tornado/website/markdown/postprocessors.py
@@ -0,0 +1,77 @@
+"""
+POST-PROCESSORS
+=============================================================================
+
+Markdown also allows post-processors, which are similar to preprocessors in
+that they need to implement a "run" method. However, they are run after core
+processing.
+
+"""
+
+
+import markdown
+
+class Processor:
+ def __init__(self, markdown_instance=None):
+ if markdown_instance:
+ self.markdown = markdown_instance
+
+class Postprocessor(Processor):
+ """
+ Postprocessors are run after the ElementTree it converted back into text.
+
+ Each Postprocessor implements a "run" method that takes a pointer to a
+ text string, modifies it as necessary and returns a text string.
+
+ Postprocessors must extend markdown.Postprocessor.
+
+ """
+
+ def run(self, text):
+ """
+ Subclasses of Postprocessor should implement a `run` method, which
+ takes the html document as a single text string and returns a
+ (possibly modified) string.
+
+ """
+ pass
+
+
+class RawHtmlPostprocessor(Postprocessor):
+ """ Restore raw html to the document. """
+
+ def run(self, text):
+ """ Iterate over html stash and restore "safe" html. """
+ for i in range(self.markdown.htmlStash.html_counter):
+ html, safe = self.markdown.htmlStash.rawHtmlBlocks[i]
+ if self.markdown.safeMode and not safe:
+ if str(self.markdown.safeMode).lower() == 'escape':
+ html = self.escape(html)
+ elif str(self.markdown.safeMode).lower() == 'remove':
+ html = ''
+ else:
+ html = markdown.HTML_REMOVED_TEXT
+ if safe or not self.markdown.safeMode:
+ text = text.replace("<p>%s</p>" %
+ (markdown.preprocessors.HTML_PLACEHOLDER % i),
+ html + "\n")
+ text = text.replace(markdown.preprocessors.HTML_PLACEHOLDER % i,
+ html)
+ return text
+
+ def escape(self, html):
+ """ Basic html escaping """
+ html = html.replace('&', '&amp;')
+ html = html.replace('<', '&lt;')
+ html = html.replace('>', '&gt;')
+ return html.replace('"', '&quot;')
+
+
+class AndSubstitutePostprocessor(Postprocessor):
+ """ Restore valid entities """
+ def __init__(self):
+ pass
+
+ def run(self, text):
+ text = text.replace(markdown.AMP_SUBSTITUTE, "&")
+ return text
diff --git a/vendor/tornado/website/markdown/preprocessors.py b/vendor/tornado/website/markdown/preprocessors.py
new file mode 100644
index 0000000000..712a1e8755
--- /dev/null
+++ b/vendor/tornado/website/markdown/preprocessors.py
@@ -0,0 +1,214 @@
+
+"""
+PRE-PROCESSORS
+=============================================================================
+
+Preprocessors work on source text before we start doing anything too
+complicated.
+"""
+
+import re
+import markdown
+
+HTML_PLACEHOLDER_PREFIX = markdown.STX+"wzxhzdk:"
+HTML_PLACEHOLDER = HTML_PLACEHOLDER_PREFIX + "%d" + markdown.ETX
+
+class Processor:
+ def __init__(self, markdown_instance=None):
+ if markdown_instance:
+ self.markdown = markdown_instance
+
+class Preprocessor (Processor):
+ """
+ Preprocessors are run after the text is broken into lines.
+
+ Each preprocessor implements a "run" method that takes a pointer to a
+ list of lines of the document, modifies it as necessary and returns
+ either the same pointer or a pointer to a new list.
+
+ Preprocessors must extend markdown.Preprocessor.
+
+ """
+ def run(self, lines):
+ """
+ Each subclass of Preprocessor should override the `run` method, which
+ takes the document as a list of strings split by newlines and returns
+ the (possibly modified) list of lines.
+
+ """
+ pass
+
+class HtmlStash:
+ """
+ This class is used for stashing HTML objects that we extract
+ in the beginning and replace with place-holders.
+ """
+
+ def __init__ (self):
+ """ Create a HtmlStash. """
+ self.html_counter = 0 # for counting inline html segments
+ self.rawHtmlBlocks=[]
+
+ def store(self, html, safe=False):
+ """
+ Saves an HTML segment for later reinsertion. Returns a
+ placeholder string that needs to be inserted into the
+ document.
+
+ Keyword arguments:
+
+ * html: an html segment
+ * safe: label an html segment as safe for safemode
+
+ Returns : a placeholder string
+
+ """
+ self.rawHtmlBlocks.append((html, safe))
+ placeholder = HTML_PLACEHOLDER % self.html_counter
+ self.html_counter += 1
+ return placeholder
+
+ def reset(self):
+ self.html_counter = 0
+ self.rawHtmlBlocks = []
+
+
+class HtmlBlockPreprocessor(Preprocessor):
+ """Remove html blocks from the text and store them for later retrieval."""
+
+ right_tag_patterns = ["</%s>", "%s>"]
+
+ def _get_left_tag(self, block):
+ return block[1:].replace(">", " ", 1).split()[0].lower()
+
+ def _get_right_tag(self, left_tag, block):
+ for p in self.right_tag_patterns:
+ tag = p % left_tag
+ i = block.rfind(tag)
+ if i > 2:
+ return tag.lstrip("<").rstrip(">"), i + len(p)-2 + len(left_tag)
+ return block.rstrip()[-len(left_tag)-2:-1].lower(), len(block)
+
+ def _equal_tags(self, left_tag, right_tag):
+ if left_tag == 'div' or left_tag[0] in ['?', '@', '%']: # handle PHP, etc.
+ return True
+ if ("/" + left_tag) == right_tag:
+ return True
+ if (right_tag == "--" and left_tag == "--"):
+ return True
+ elif left_tag == right_tag[1:] \
+ and right_tag[0] != "<":
+ return True
+ else:
+ return False
+
+ def _is_oneliner(self, tag):
+ return (tag in ['hr', 'hr/'])
+
+ def run(self, lines):
+ text = "\n".join(lines)
+ new_blocks = []
+ text = text.split("\n\n")
+ items = []
+ left_tag = ''
+ right_tag = ''
+ in_tag = False # flag
+
+ while text:
+ block = text[0]
+ if block.startswith("\n"):
+ block = block[1:]
+ text = text[1:]
+
+ if block.startswith("\n"):
+ block = block[1:]
+
+ if not in_tag:
+ if block.startswith("<"):
+ left_tag = self._get_left_tag(block)
+ right_tag, data_index = self._get_right_tag(left_tag, block)
+
+ if data_index < len(block):
+ text.insert(0, block[data_index:])
+ block = block[:data_index]
+
+ if not (markdown.isBlockLevel(left_tag) \
+ or block[1] in ["!", "?", "@", "%"]):
+ new_blocks.append(block)
+ continue
+
+ if self._is_oneliner(left_tag):
+ new_blocks.append(block.strip())
+ continue
+
+ if block[1] == "!":
+ # is a comment block
+ left_tag = "--"
+ right_tag, data_index = self._get_right_tag(left_tag, block)
+ # keep checking conditions below and maybe just append
+
+ if block.rstrip().endswith(">") \
+ and self._equal_tags(left_tag, right_tag):
+ new_blocks.append(
+ self.markdown.htmlStash.store(block.strip()))
+ continue
+ else: #if not block[1] == "!":
+ # if is block level tag and is not complete
+
+ if markdown.isBlockLevel(left_tag) or left_tag == "--" \
+ and not block.rstrip().endswith(">"):
+ items.append(block.strip())
+ in_tag = True
+ else:
+ new_blocks.append(
+ self.markdown.htmlStash.store(block.strip()))
+
+ continue
+
+ new_blocks.append(block)
+
+ else:
+ items.append(block.strip())
+
+ right_tag, data_index = self._get_right_tag(left_tag, block)
+
+ if self._equal_tags(left_tag, right_tag):
+ # if find closing tag
+ in_tag = False
+ new_blocks.append(
+ self.markdown.htmlStash.store('\n\n'.join(items)))
+ items = []
+
+ if items:
+ new_blocks.append(self.markdown.htmlStash.store('\n\n'.join(items)))
+ new_blocks.append('\n')
+
+ new_text = "\n\n".join(new_blocks)
+ return new_text.split("\n")
+
+
+class ReferencePreprocessor(Preprocessor):
+ """ Remove reference definitions from text and store for later use. """
+
+ RE = re.compile(r'^(\ ?\ ?\ ?)\[([^\]]*)\]:\s*([^ ]*)(.*)$', re.DOTALL)
+
+ def run (self, lines):
+ new_text = [];
+ for line in lines:
+ m = self.RE.match(line)
+ if m:
+ id = m.group(2).strip().lower()
+ t = m.group(4).strip() # potential title
+ if not t:
+ self.markdown.references[id] = (m.group(3), t)
+ elif (len(t) >= 2
+ and (t[0] == t[-1] == "\""
+ or t[0] == t[-1] == "\'"
+ or (t[0] == "(" and t[-1] == ")") ) ):
+ self.markdown.references[id] = (m.group(3), t[1:-1])
+ else:
+ new_text.append(line)
+ else:
+ new_text.append(line)
+
+ return new_text #+ "\n"
diff --git a/vendor/tornado/website/markdown/treeprocessors.py b/vendor/tornado/website/markdown/treeprocessors.py
new file mode 100644
index 0000000000..1dc612a95e
--- /dev/null
+++ b/vendor/tornado/website/markdown/treeprocessors.py
@@ -0,0 +1,329 @@
+import markdown
+import re
+
+def isString(s):
+ """ Check if it's string """
+ return isinstance(s, unicode) or isinstance(s, str)
+
+class Processor:
+ def __init__(self, markdown_instance=None):
+ if markdown_instance:
+ self.markdown = markdown_instance
+
+class Treeprocessor(Processor):
+ """
+ Treeprocessors are run on the ElementTree object before serialization.
+
+ Each Treeprocessor implements a "run" method that takes a pointer to an
+ ElementTree, modifies it as necessary and returns an ElementTree
+ object.
+
+ Treeprocessors must extend markdown.Treeprocessor.
+
+ """
+ def run(self, root):
+ """
+ Subclasses of Treeprocessor should implement a `run` method, which
+ takes a root ElementTree. This method can return another ElementTree
+ object, and the existing root ElementTree will be replaced, or it can
+ modify the current tree and return None.
+ """
+ pass
+
+
+class InlineProcessor(Treeprocessor):
+ """
+ A Treeprocessor that traverses a tree, applying inline patterns.
+ """
+
+ def __init__ (self, md):
+ self.__placeholder_prefix = markdown.INLINE_PLACEHOLDER_PREFIX
+ self.__placeholder_suffix = markdown.ETX
+ self.__placeholder_length = 4 + len(self.__placeholder_prefix) \
+ + len(self.__placeholder_suffix)
+ self.__placeholder_re = re.compile(markdown.INLINE_PLACEHOLDER % r'([0-9]{4})')
+ self.markdown = md
+
+ def __makePlaceholder(self, type):
+ """ Generate a placeholder """
+ id = "%04d" % len(self.stashed_nodes)
+ hash = markdown.INLINE_PLACEHOLDER % id
+ return hash, id
+
+ def __findPlaceholder(self, data, index):
+ """
+ Extract id from data string, start from index
+
+ Keyword arguments:
+
+ * data: string
+ * index: index, from which we start search
+
+ Returns: placeholder id and string index, after the found placeholder.
+ """
+
+ m = self.__placeholder_re.search(data, index)
+ if m:
+ return m.group(1), m.end()
+ else:
+ return None, index + 1
+
+ def __stashNode(self, node, type):
+ """ Add node to stash """
+ placeholder, id = self.__makePlaceholder(type)
+ self.stashed_nodes[id] = node
+ return placeholder
+
+ def __handleInline(self, data, patternIndex=0):
+ """
+ Process string with inline patterns and replace it
+ with placeholders
+
+ Keyword arguments:
+
+ * data: A line of Markdown text
+ * patternIndex: The index of the inlinePattern to start with
+
+ Returns: String with placeholders.
+
+ """
+ if not isinstance(data, markdown.AtomicString):
+ startIndex = 0
+ while patternIndex < len(self.markdown.inlinePatterns):
+ data, matched, startIndex = self.__applyPattern(
+ self.markdown.inlinePatterns.value_for_index(patternIndex),
+ data, patternIndex, startIndex)
+ if not matched:
+ patternIndex += 1
+ return data
+
+ def __processElementText(self, node, subnode, isText=True):
+ """
+ Process placeholders in Element.text or Element.tail
+ of Elements popped from self.stashed_nodes.
+
+ Keywords arguments:
+
+ * node: parent node
+ * subnode: processing node
+ * isText: bool variable, True - it's text, False - it's tail
+
+ Returns: None
+
+ """
+ if isText:
+ text = subnode.text
+ subnode.text = None
+ else:
+ text = subnode.tail
+ subnode.tail = None
+
+ childResult = self.__processPlaceholders(text, subnode)
+
+ if not isText and node is not subnode:
+ pos = node.getchildren().index(subnode)
+ node.remove(subnode)
+ else:
+ pos = 0
+
+ childResult.reverse()
+ for newChild in childResult:
+ node.insert(pos, newChild)
+
+ def __processPlaceholders(self, data, parent):
+ """
+ Process string with placeholders and generate ElementTree tree.
+
+ Keyword arguments:
+
+ * data: string with placeholders instead of ElementTree elements.
+ * parent: Element, which contains processing inline data
+
+ Returns: list with ElementTree elements with applied inline patterns.
+ """
+ def linkText(text):
+ if text:
+ if result:
+ if result[-1].tail:
+ result[-1].tail += text
+ else:
+ result[-1].tail = text
+ else:
+ if parent.text:
+ parent.text += text
+ else:
+ parent.text = text
+
+ result = []
+ strartIndex = 0
+ while data:
+ index = data.find(self.__placeholder_prefix, strartIndex)
+ if index != -1:
+ id, phEndIndex = self.__findPlaceholder(data, index)
+
+ if id in self.stashed_nodes:
+ node = self.stashed_nodes.get(id)
+
+ if index > 0:
+ text = data[strartIndex:index]
+ linkText(text)
+
+ if not isString(node): # it's Element
+ for child in [node] + node.getchildren():
+ if child.tail:
+ if child.tail.strip():
+ self.__processElementText(node, child, False)
+ if child.text:
+ if child.text.strip():
+ self.__processElementText(child, child)
+ else: # it's just a string
+ linkText(node)
+ strartIndex = phEndIndex
+ continue
+
+ strartIndex = phEndIndex
+ result.append(node)
+
+ else: # wrong placeholder
+ end = index + len(prefix)
+ linkText(data[strartIndex:end])
+ strartIndex = end
+ else:
+ text = data[strartIndex:]
+ linkText(text)
+ data = ""
+
+ return result
+
+ def __applyPattern(self, pattern, data, patternIndex, startIndex=0):
+ """
+ Check if the line fits the pattern, create the necessary
+ elements, add it to stashed_nodes.
+
+ Keyword arguments:
+
+ * data: the text to be processed
+ * pattern: the pattern to be checked
+ * patternIndex: index of current pattern
+ * startIndex: string index, from which we starting search
+
+ Returns: String with placeholders instead of ElementTree elements.
+
+ """
+ match = pattern.getCompiledRegExp().match(data[startIndex:])
+ leftData = data[:startIndex]
+
+ if not match:
+ return data, False, 0
+
+ node = pattern.handleMatch(match)
+
+ if node is None:
+ return data, True, len(leftData) + match.span(len(match.groups()))[0]
+
+ if not isString(node):
+ if not isinstance(node.text, markdown.AtomicString):
+ # We need to process current node too
+ for child in [node] + node.getchildren():
+ if not isString(node):
+ if child.text:
+ child.text = self.__handleInline(child.text,
+ patternIndex + 1)
+ if child.tail:
+ child.tail = self.__handleInline(child.tail,
+ patternIndex)
+
+ placeholder = self.__stashNode(node, pattern.type())
+
+ return "%s%s%s%s" % (leftData,
+ match.group(1),
+ placeholder, match.groups()[-1]), True, 0
+
+ def run(self, tree):
+ """Apply inline patterns to a parsed Markdown tree.
+
+ Iterate over ElementTree, find elements with inline tag, apply inline
+ patterns and append newly created Elements to tree. If you don't
+ want process your data with inline paterns, instead of normal string,
+ use subclass AtomicString:
+
+ node.text = markdown.AtomicString("data won't be processed with inline patterns")
+
+ Arguments:
+
+ * markdownTree: ElementTree object, representing Markdown tree.
+
+ Returns: ElementTree object with applied inline patterns.
+
+ """
+ self.stashed_nodes = {}
+
+ stack = [tree]
+
+ while stack:
+ currElement = stack.pop()
+ insertQueue = []
+ for child in currElement.getchildren():
+ if child.text and not isinstance(child.text, markdown.AtomicString):
+ text = child.text
+ child.text = None
+ lst = self.__processPlaceholders(self.__handleInline(
+ text), child)
+ stack += lst
+ insertQueue.append((child, lst))
+
+ if child.getchildren():
+ stack.append(child)
+
+ for element, lst in insertQueue:
+ if element.text:
+ element.text = \
+ markdown.inlinepatterns.handleAttributes(element.text,
+ element)
+ i = 0
+ for newChild in lst:
+ # Processing attributes
+ if newChild.tail:
+ newChild.tail = \
+ markdown.inlinepatterns.handleAttributes(newChild.tail,
+ element)
+ if newChild.text:
+ newChild.text = \
+ markdown.inlinepatterns.handleAttributes(newChild.text,
+ newChild)
+ element.insert(i, newChild)
+ i += 1
+ return tree
+
+
+class PrettifyTreeprocessor(Treeprocessor):
+ """ Add linebreaks to the html document. """
+
+ def _prettifyETree(self, elem):
+ """ Recursively add linebreaks to ElementTree children. """
+
+ i = "\n"
+ if markdown.isBlockLevel(elem.tag) and elem.tag not in ['code', 'pre']:
+ if (not elem.text or not elem.text.strip()) \
+ and len(elem) and markdown.isBlockLevel(elem[0].tag):
+ elem.text = i
+ for e in elem:
+ if markdown.isBlockLevel(e.tag):
+ self._prettifyETree(e)
+ if not elem.tail or not elem.tail.strip():
+ elem.tail = i
+ if not elem.tail or not elem.tail.strip():
+ elem.tail = i
+
+ def run(self, root):
+ """ Add linebreaks to ElementTree root object. """
+
+ self._prettifyETree(root)
+ # Do <br />'s seperately as they are often in the middle of
+ # inline content and missed by _prettifyETree.
+ brs = root.getiterator('br')
+ for br in brs:
+ if not br.tail or not br.tail.strip():
+ br.tail = '\n'
+ else:
+ br.tail = '\n%s' % br.tail
diff --git a/vendor/tornado/website/static/base.css b/vendor/tornado/website/static/base.css
new file mode 100644
index 0000000000..543d6f24c3
--- /dev/null
+++ b/vendor/tornado/website/static/base.css
@@ -0,0 +1,120 @@
+body {
+ background: white;
+ color: black;
+ font-family: Georgia, serif;
+ font-size: 11pt;
+ margin: 10px;
+ margin-top: 15px;
+ margin-bottom: 15px;
+}
+
+h1,
+h2,
+h3,
+h4 {
+ font-family: Calibri, sans-serif;
+ margin: 0;
+}
+
+img {
+ border: 0;
+}
+
+pre,
+code {
+ color: #060;
+}
+
+a,
+a code {
+ color: #216093;
+}
+
+table {
+ border-collapse: collapse;
+ border: 0;
+}
+
+td {
+ border: 0;
+ padding: 0;
+}
+
+#body {
+ margin: auto;
+ max-width: 850px;
+}
+
+#header {
+ margin-bottom: 15px;
+ margin-right: 30px;
+}
+
+#content,
+#footer {
+ margin-left: 31px;
+ margin-right: 31px;
+}
+
+#content p,
+#content li,
+#footer {
+ line-height: 16pt;
+}
+
+#content pre {
+ line-height: 14pt;
+ margin: 17pt;
+ padding-left: 1em;
+ border-left: 1px solid #ccc;
+}
+
+#footer {
+ margin-top: 5em;
+}
+
+#header .logo {
+ line-height: 0;
+ padding-bottom: 5px;
+ padding-right: 15px;
+}
+
+#header .logo img {
+ width: 286px;
+ height: 72px;
+}
+
+#header .title {
+ vertical-align: bottom;
+}
+
+#header .title h1 {
+ font-size: 35px;
+ font-weight: normal;
+}
+
+#header .title h1,
+#header .title h1 a {
+ color: #666;
+}
+
+#content h1,
+#content h2,
+#content h3 {
+ color: #4d8cbf;
+ margin-bottom: 2pt;
+ margin-top: 17pt;
+}
+
+#content h2 {
+ font-size: 19pt;
+}
+
+#content h3 {
+ font-size: 15pt;
+}
+
+#content p {
+ margin: 0;
+ margin-bottom: 1em;
+}
diff --git a/vendor/tornado/website/static/facebook.png b/vendor/tornado/website/static/facebook.png
new file mode 100755
index 0000000000..47738323ed
--- /dev/null
+++ b/vendor/tornado/website/static/facebook.png
Binary files differ
diff --git a/vendor/tornado/website/static/friendfeed.png b/vendor/tornado/website/static/friendfeed.png
new file mode 100755
index 0000000000..ac09f4e453
--- /dev/null
+++ b/vendor/tornado/website/static/friendfeed.png
Binary files differ
diff --git a/vendor/tornado/website/static/robots.txt b/vendor/tornado/website/static/robots.txt
new file mode 100644
index 0000000000..0ad279c736
--- /dev/null
+++ b/vendor/tornado/website/static/robots.txt
@@ -0,0 +1,2 @@
+User-Agent: *
+Disallow:
diff --git a/vendor/tornado/website/static/tornado-0.1.tar.gz b/vendor/tornado/website/static/tornado-0.1.tar.gz
new file mode 100644
index 0000000000..d06c1e04ea
--- /dev/null
+++ b/vendor/tornado/website/static/tornado-0.1.tar.gz
Binary files differ
diff --git a/vendor/tornado/website/static/tornado-0.2.tar.gz b/vendor/tornado/website/static/tornado-0.2.tar.gz
new file mode 100644
index 0000000000..6aca327aee
--- /dev/null
+++ b/vendor/tornado/website/static/tornado-0.2.tar.gz
Binary files differ
diff --git a/vendor/tornado/website/static/tornado.png b/vendor/tornado/website/static/tornado.png
new file mode 100644
index 0000000000..a920aa566f
--- /dev/null
+++ b/vendor/tornado/website/static/tornado.png
Binary files differ
diff --git a/vendor/tornado/website/static/twitter.png b/vendor/tornado/website/static/twitter.png
new file mode 100755
index 0000000000..5099c62ee5
--- /dev/null
+++ b/vendor/tornado/website/static/twitter.png
Binary files differ
diff --git a/vendor/tornado/website/templates/base.html b/vendor/tornado/website/templates/base.html
new file mode 100644
index 0000000000..48da016781
--- /dev/null
+++ b/vendor/tornado/website/templates/base.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>{% block title %}Tornado Web Server{% end %}</title>
+ <link rel="stylesheet" href="/static/base.css" type="text/css"/>
+ {% block head %}{% end %}
+ </head>
+ <body>
+ <div id="body">
+ <div id="header">
+ <table>
+ <tr>
+ <td class="logo"><a href="/"><img src="/static/tornado.png" alt="Tornado"/></a></td>
+ <td class="title">{% block headertitle %}{% end %}</td>
+ </tr>
+ </table>
+ </div>
+ <div id="content">{% block body %}{% end %}</div>
+ <div id="footer">
+ <div>Tornado is one of <a href="http://developers.facebook.com/opensource.php">Facebook's open source technologies</a>. It is available under the <a href="http://www.apache.org/licenses/LICENSE-2.0.html">Apache Licence, Version 2.0</a>.</div>
+ <div>This web site and all documentation is licensed under <a href="http://creativecommons.org/licenses/by/3.0/">Creative Commons 3.0</a>.</div>
+ </div>
+ </div>
+ {% block bottom %}{% end %}
+ </body>
+</html>
diff --git a/vendor/tornado/website/templates/documentation.html b/vendor/tornado/website/templates/documentation.html
new file mode 100644
index 0000000000..8c28740874
--- /dev/null
+++ b/vendor/tornado/website/templates/documentation.html
@@ -0,0 +1,9 @@
+{% extends "base.html" %}
+
+{% block title %}Tornado Web Server Documentation{% end %}
+
+{% block headertitle %}<h1>documentation</h1>{% end %}
+
+{% block body %}
+ {{ markdown("documentation.txt", toc=True) }}
+{% end %}
diff --git a/vendor/tornado/website/templates/documentation.txt b/vendor/tornado/website/templates/documentation.txt
new file mode 100644
index 0000000000..81262a605a
--- /dev/null
+++ b/vendor/tornado/website/templates/documentation.txt
@@ -0,0 +1,866 @@
+Overview
+--------
+[FriendFeed](http://friendfeed.com/)'s web server is a relatively simple,
+non-blocking web server written in Python. The FriendFeed application is
+written using a web framework that looks a bit like
+[web.py](http://webpy.org/) or Google's
+[webapp](http://code.google.com/appengine/docs/python/tools/webapp/),
+but with additional tools and optimizations to take advantage of the
+non-blocking web server and tools.
+
+[Tornado](http://github.com/facebook/tornado) is an open source
+version of this web server and some of the tools we use most often at
+FriendFeed. The framework is distinct from most mainstream web server
+frameworks (and certainly most Python frameworks) because it is
+non-blocking and reasonably fast. Because it is non-blocking
+and uses [epoll](http://www.kernel.org/doc/man-pages/online/pages/man4/epoll.4.html), it can handle 1000s of simultaneous standing connections,
+which means the framework is ideal for real-time web services. We built the
+web server specifically to handle FriendFeed's real-time features &mdash;
+every active user of FriendFeed maintains an open connection to the
+FriendFeed servers. (For more information on scaling servers to support
+thousands of clients, see
+[The C10K problem](http://www.kegel.com/c10k.html).)
+
+Here is the canonical "Hello, world" example app:
+
+ import tornado.httpserver
+ import tornado.ioloop
+ import tornado.web
+
+ class MainHandler(tornado.web.RequestHandler):
+ def get(self):
+ self.write("Hello, world")
+
+ application = tornado.web.Application([
+ (r"/", MainHandler),
+ ])
+
+ if __name__ == "__main__":
+ http_server = tornado.httpserver.HTTPServer(application)
+ http_server.listen(8888)
+ tornado.ioloop.IOLoop.instance().start()
+
+See [Tornado walkthrough](#tornado-walkthrough) below for a detailed
+walkthrough of the `tornado.web` package.
+
+We attempted to clean up the code base to reduce interdependencies between
+modules, so you should (theoretically) be able to use any of the modules
+independently in your project without using the whole package.
+
+
+Download
+--------
+Download the most recent version of Tornado from GitHub:
+
+> [tornado-0.2.tar.gz](/static/tornado-0.2.tar.gz)
+
+You can also [browse the source](http://github.com/facebook/tornado) on GitHub. To install Tornado:
+
+ tar xvzf tornado-0.2.tar.gz
+ cd tornado-0.2
+ python setup.py build
+ sudo python setup.py install
+
+After installation, you should be able to run any of the demos in the `demos`
+directory included with the Tornado package.
+
+ ./demos/helloworld/helloworld.py
+
+### Prerequisites
+
+Tornado has been tested on Python 2.5 and 2.6. To use all of the features of Tornado, you need to have [PycURL](http://pycurl.sourceforge.net/) and a JSON library like [simplejson](http://pypi.python.org/pypi/simplejson/) installed. Complete installation instructions for Mac OS X and Ubuntu are included below for convenience.
+
+**Mac OS X 10.5/10.6**
+
+ sudo easy_install setuptools pycurl==7.16.2.1 simplejson
+
+**Ubuntu Linux**
+
+ sudo apt-get install python-dev python-pycurl python-simplejson
+
+
+Module index
+------------
+The most important module is [`web`](http://github.com/facebook/tornado/blob/master/tornado/web.py), which is the web framework
+that includes most of the meat of the Tornado package. The other modules
+are tools that make `web` more useful. See
+[Tornado walkthrough](#tornado-walkthrough) below for a detailed
+walkthrough of the `web` package.
+
+### Main modules
+ * [`web`](http://github.com/facebook/tornado/blob/master/tornado/web.py) - The web framework on which FriendFeed is built. `web` incorporates most of the important features of Tornado
+ * [`escape`](http://github.com/facebook/tornado/blob/master/tornado/escape.py) - XHTML, JSON, and URL encoding/decoding methods
+ * [`database`](http://github.com/facebook/tornado/blob/master/tornado/database.py) - A simple wrapper around `MySQLdb` to make MySQL easier to use
+ * [`template`](http://github.com/facebook/tornado/blob/master/tornado/template.py) - A Python-based web templating language
+ * [`httpclient`](http://github.com/facebook/tornado/blob/master/tornado/httpclient.py) - A non-blocking HTTP client designed to work with `web` and `httpserver`
+ * [`auth`](http://github.com/facebook/tornado/blob/master/tornado/auth.py) - Implementation of third party authentication and authorization schemes (Google OpenID/OAuth, Facebook Platform, Yahoo BBAuth, FriendFeed OpenID/OAuth, Twitter OAuth)
+ * [`locale`](http://github.com/facebook/tornado/blob/master/tornado/locale.py) - Localization/translation support
+ * [`options`](http://github.com/facebook/tornado/blob/master/tornado/options.py) - Command line and config file parsing, optimized for server environments
+
+### Low-level modules
+ * [`httpserver`](http://github.com/facebook/tornado/blob/master/tornado/httpserver.py) - A very simple HTTP server built on which `web` is built
+ * [`iostream`](http://github.com/facebook/tornado/blob/master/tornado/iostream.py) - A simple wrapper around non-blocking sockets to aide common reading and writing patterns
+ * [`ioloop`](http://github.com/facebook/tornado/blob/master/tornado/ioloop.py) - Core I/O loop
+
+### Random modules
+ * [`s3server`](http://github.com/facebook/tornado/blob/master/tornado/s3server.py) - A web server that implements most of the [Amazon S3](http://aws.amazon.com/s3/) interface, backed by local file storage
+
+
+Tornado walkthrough
+-------------------
+
+### Request handlers and request arguments
+
+A Tornado web application maps URLs or URL patterns to subclasses of
+`tornado.web.RequestHandler`. Those classes define `get()` or `post()`
+methods to handle HTTP `GET` or `POST` requests to that URL.
+
+This code maps the root URL `/` to `MainHandler` and the URL pattern
+`/story/([0-9]+)` to `StoryHandler`. Regular expression groups are passed
+as arguments to the `RequestHandler` methods:
+
+ class MainHandler(tornado.web.RequestHandler):
+ def get(self):
+ self.write("You requested the main page")
+
+ class StoryHandler(tornado.web.RequestHandler):
+ def get(self, story_id):
+ self.write("You requested the story " + story_id)
+
+ application = tornado.web.Application([
+ (r"/", MainHandler),
+ (r"/story/([0-9]+)", StoryHandler),
+ ])
+
+You can get query string arguments and parse `POST` bodies with the
+`get_argument()` method:
+
+ class MainHandler(tornado.web.RequestHandler):
+ def get(self):
+ self.write('<html><body><form action="/" method="post">'
+ '<input type="text" name="message">'
+ '<input type="submit" value="Submit">'
+ '</form></body></html>')
+
+ def post(self):
+ self.set_header("Content-Type", "text/plain")
+ self.write("You wrote " + self.get_argument("message"))
+
+If you want to send an error response to the client, e.g., 403 Unauthorized,
+you can just raise a `tornado.web.HTTPError` exception:
+
+ if not self.user_is_logged_in():
+ raise tornado.web.HTTPError(403)
+
+The request handler can access the object representing the current request
+with `self.request`. The `HTTPRequest` object includes a number of useful
+attribute, including:
+
+ * `arguments` - all of the `GET` and `POST` arguments
+ * `files` - all of the uploaded files (via `multipart/form-data` POST requests)
+ * `path` - the request path (everything before the `?`)
+ * `headers` - the request headers
+
+See the class definition for `HTTPRequest` in `httpserver` for a complete list
+of attributes.
+
+
+### Templates
+
+You can use any template language supported by Python, but Tornado ships
+with its own templating language that is a lot faster and more flexible
+than many of the most popular templating systems out there. See the
+[`template`](http://github.com/facebook/tornado/blob/master/tornado/template.py) module documentation for complete documentation.
+
+A Tornado template is just HTML (or any other text-based format) with
+Python control sequences and expressions embedded within the markup:
+
+ <html>
+ <head>
+ <title>{{ title }}</title>
+ </head>
+ <body>
+ <ul>
+ {% for item in items %}
+ <li>{{ escape(item) }}</li>
+ {% end %}
+ </ul>
+ </body>
+ </html>
+
+If you saved this template as "template.html" and put it in the same
+directory as your Python file, you could render this template with:
+
+ class MainHandler(tornado.web.RequestHandler):
+ def get(self):
+ items = ["Item 1", "Item 2", "Item 3"]
+ self.render("template.html", title="My title", items=items)
+
+Tornado templates support *control statements* and *expressions*. Control
+statements are surronded by `{%` and `%}`, e.g., `{% if len(items) > 2 %}`.
+Expressions are surrounded by `{{` and `}}`, e.g., `{{ items[0] }}`.
+
+Control statements more or less map exactly to Python statements. We support
+`if`, `for`, `while`, and `try`, all of which are terminated with `{% end %}`.
+We also support *template inheritance* using the `extends` and `block`
+statements, which are described in detail in the documentation for the
+[`template` module](http://github.com/facebook/tornado/blob/master/tornado/template.py).
+
+Expressions can be any Python expression, including function calls. We
+support the functions `escape`, `url_escape`, and `json_encode` by default,
+and you can pass other functions into the template simply by passing them
+as keyword arguments to the template render function:
+
+ class MainHandler(tornado.web.RequestHandler):
+ def get(self):
+ self.render("template.html", add=self.add)
+
+ def add(self, x, y):
+ return x + y
+
+When you are building a real application, you are going to want to use
+all of the features of Tornado templates, especially template inheritance.
+Read all about those features in the [`template` module](http://github.com/facebook/tornado/blob/master/tornado/template.py)
+section.
+
+Under the hood, Tornado templates are translated directly to Python.
+The expressions you include in your template are copied verbatim into
+a Python function representing your template. We don't try to prevent
+anything in the template language; we created it explicitly to provide
+the flexibility that other, stricter templating systems prevent.
+Consequently, if you write random stuff inside of your template expressions,
+you will get random Python errors when you execute the template.
+
+
+### Cookies and secure cookies
+
+You can set cookies in the user's browser with the `set_cookie` method:
+
+ class MainHandler(tornado.web.RequestHandler):
+ def get(self):
+ if not self.get_cookie("mycookie"):
+ self.set_cookie("mycookie", "myvalue")
+ self.write("Your cookie was not set yet!")
+ else:
+ self.write("Your cookie was set!")
+
+Cookies are easily forged by malicious clients. If you need to set cookies
+to, e.g., save the user ID of the currently logged in user, you need to
+sign your cookies to prevent forgery. Tornado supports this out of the
+box with the `set_secure_cookie` and `get_secure_cookie` methods. To use
+these methods, you need to specify a secret key named `cookie_secret` when
+you create your application. You can pass in application settings as keyword
+arguments to your application:
+
+ application = tornado.web.Application([
+ (r"/", MainHandler),
+ ], cookie_secret="61oETzKXQAGaYdkL5gEmGeJJFuYh7EQnp2XdTP1o/Vo=")
+
+Signed cookies contain the encoded value of the cookie in addition to a
+timestamp and an [HMAC](http://en.wikipedia.org/wiki/HMAC) signature. If the
+cookie is old or if the signature doesn't match, `get_secure_cookie` will
+return `None` just as if the cookie isn't set. The secure version of the
+example above:
+
+ class MainHandler(tornado.web.RequestHandler):
+ def get(self):
+ if not self.get_secure_cookie("mycookie"):
+ self.set_secure_cookie("mycookie", "myvalue")
+ self.write("Your cookie was not set yet!")
+ else:
+ self.write("Your cookie was set!")
+
+
+### User authentication
+
+The currently authenticated user is available in every request handler
+as `self.current_user`, and in every template as `current_user`. By
+default, `current_user` is `None`.
+
+To implement user authentication in your application, you need to
+override the `get_current_user()` method in your request handlers to
+determine the current user based on, e.g., the value of a cookie.
+Here is an example that lets users log into the application simply
+by specifying a nickname, which is then saved in a cookie:
+
+ class BaseHandler(tornado.web.RequestHandler):
+ def get_current_user(self):
+ return self.get_secure_cookie("user")
+
+ class MainHandler(BaseHandler):
+ def get(self):
+ if not self.current_user:
+ self.redirect("/login")
+ return
+ name = tornado.escape.xhtml_escape(self.current_user)
+ self.write("Hello, " + name)
+
+ class LoginHandler(BaseHandler):
+ def get(self):
+ self.write('<html><body><form action="/login" method="post">'
+ 'Name: <input type="text" name="name">'
+ '<input type="submit" value="Sign in">'
+ '</form></body></html>')
+
+ def post(self):
+ self.set_secure_cookie("user", self.get_argument("name"))
+ self.redirect("/")
+
+ application = tornado.web.Application([
+ (r"/", MainHandler),
+ (r"/login", LoginHandler),
+ ], cookie_secret="61oETzKXQAGaYdkL5gEmGeJJFuYh7EQnp2XdTP1o/Vo=")
+
+You can require that the user be logged in using the
+[Python decorator](http://www.python.org/dev/peps/pep-0318/)
+`tornado.web.authenticated`. If a request goes to a method with this
+decorator, and the user is not logged in, they will be redirected to
+`login_url` (another application setting). The example above could
+be rewritten:
+
+ class MainHandler(BaseHandler):
+ @tornado.web.authenticated
+ def get(self):
+ name = tornado.escape.xhtml_escape(self.current_user)
+ self.write("Hello, " + name)
+
+ settings = {
+ "cookie_secret": "61oETzKXQAGaYdkL5gEmGeJJFuYh7EQnp2XdTP1o/Vo=",
+ "login_url": "/login",
+ }
+ application = tornado.web.Application([
+ (r"/", MainHandler),
+ (r"/login", LoginHandler),
+ ], **settings)
+
+If you decorate `post()` methods with the `authenticated` decorator, and
+the user is not logged in, the server will send a `403` response.
+
+Tornado comes with built-in support for third-party authentication schemes
+like Google OAuth. See the [`auth` module](http://github.com/facebook/tornado/blob/master/tornado/auth.py) for more details. Check
+out the Tornado Blog example application for a complete example that
+uses authentication (and stores user data in a MySQL database).
+
+
+### Cross-site request forgery protection
+
+[Cross-site request forgery](http://en.wikipedia.org/wiki/Cross-site_request_forgery), or XSRF, is a common problem for personalized web applications. See the
+[Wikipedia article](http://en.wikipedia.org/wiki/Cross-site_request_forgery)
+for more information on how XSRF works.
+
+The generally accepted solution to prevent XSRF is to cookie every user
+with an unpredictable value and include that value as an additional
+argument with every form submission on your site. If the cookie and the
+value in the form submission do not match, then the request is likely
+forged.
+
+Tornado comes with built-in XSRF protection. To include it in your site,
+include the application setting `xsrf_cookies`:
+
+ settings = {
+ "cookie_secret": "61oETzKXQAGaYdkL5gEmGeJJFuYh7EQnp2XdTP1o/Vo=",
+ "login_url": "/login",
+ "xsrf_cookies": True,
+ }
+ application = tornado.web.Application([
+ (r"/", MainHandler),
+ (r"/login", LoginHandler),
+ ], **settings)
+
+If `xsrf_cookies` is set, the Tornado web application will set the `_xsrf`
+cookie for all users and reject all `POST` requests hat do not contain a
+correct `_xsrf` value. If you turn this setting on, you need to instrument
+all forms that submit via `POST` to contain this field. You can do this with
+the special function `xsrf_form_html()`, available in all templates:
+
+ <form action="/login" method="post">
+ {{ xsrf_form_html() }}
+ <div>Username: <input type="text" name="username"/></div>
+ <div>Password: <input type="password" name="password"/></div>
+ <div><input type="submit" value="Sign in"/></div>
+ </form>
+
+If you submit AJAX `POST` requests, you will also need to instrument your
+JavaScript to include the `_xsrf` value with each request. This is the
+[jQuery](http://jquery.com/) function we use at FriendFeed for AJAX `POST`
+requests that automatically adds the `_xsrf` value to all requests:
+
+ function getCookie(name) {
+ var r = document.cookie.match("\\b" + name + "=([^;]*)\\b");
+ return r ? r[1] : undefined;
+ }
+
+ jQuery.postJSON = function(url, args, callback) {
+ args._xsrf = getCookie("_xsrf");
+ $.ajax({url: url, data: $.param(args), dataType: "text", type: "POST",
+ success: function(response) {
+ callback(eval("(" + response + ")"));
+ }});
+ };
+
+
+### Static files and aggressive file caching
+
+You can serve static files from Tornado by specifying the `static_path`
+setting in your application:
+
+ settings = {
+ "static_path": os.path.join(os.path.dirname(__file__), "static"),
+ "cookie_secret": "61oETzKXQAGaYdkL5gEmGeJJFuYh7EQnp2XdTP1o/Vo=",
+ "login_url": "/login",
+ "xsrf_cookies": True,
+ }
+ application = tornado.web.Application([
+ (r"/", MainHandler),
+ (r"/login", LoginHandler),
+ ], **settings)
+
+This setting will automatically make all requests that start with `/static/`
+serve from that static directory, e.g., [http://localhost:8888/static/foo.png](http://localhost:8888/static/foo.png)
+will serve the file `foo.png` from the specified static directory. We
+also automatically serve `/robots.txt` and `/favicon.ico` from the static
+directory (even though they don't start with the `/static/` prefix).
+
+To improve performance, it is generally a good idea for browsers to
+cache static resources aggressively so browsers won't send unnecessary
+`If-Modified-Since` or `Etag` requests that might block the rendering of
+the page. Tornado supports this out of the box with *static content
+versioning*.
+
+To use this feature, use the `static_url()` method in your templates rather
+than typing the URL of the static file directly in your HTML:
+
+ <html>
+ <head>
+ <title>FriendFeed - {{ _("Home") }}</title>
+ </head>
+ <body>
+ <div><img src="{{ static_url("images/logo.png") }}"/></div>
+ </body>
+ </html>
+
+The `static_url()` function will translate that relative path to a URI
+that looks like `/static/images/logo.png?v=aae54`. The `v` argument is
+a hash of the content in `logo.png`, and its presence makes the Tornado
+server send cache headers to the user's browser that will make the browser
+cache the content indefinitely.
+
+Since the `v` argument is based on the content of the file, if you update
+a file and restart your server, it will start sending a new `v` value,
+so the user's browser will automatically fetch the new file. If the file's
+contents don't change, the browser will continue to use a locally cached
+copy without ever checking for updates on the server, significantly
+improving rendering performance.
+
+In production, you probably want to serve static files from a more
+optimized static file server like [nginx](http://nginx.net/). You can
+configure most any web server to support these caching semantics. Here
+is the nginx configuration we use at FriendFeed:
+
+ location /static/ {
+ root /var/friendfeed/static;
+ if ($query_string) {
+ expires max;
+ }
+ }
+
+
+### Localization
+
+The locale of the current user (whether they are logged in or not) is
+always available as `self.locale` in the request handler and as `locale`
+in templates. The name of the locale (e.g., `en_US`) is available as
+`locale.name`, and you can translate strings with the `locale.translate`
+method. Templates also have the global function call `_()` available
+for string translation. The translate function has two forms:
+
+ _("Translate this string")
+
+which translates the string directly based on the current locale, and
+
+ _("A person liked this", "%(num)d people liked this", len(people)) % {"num": len(people)}
+
+which translates a string that can be singular or plural based on the value
+of the third argument. In the example above, a translation of the first
+string will be returned if `len(people)` is `1`, or a translation of the
+second string will be returned otherwise.
+
+The most common pattern for translations is to use Python named placeholders
+for variables (the `%(num)d` in the example above) since placeholders can
+move around on translation.
+
+Here is a properly localized template:
+
+ <html>
+ <head>
+ <title>FriendFeed - {{ _("Sign in") }}</title>
+ </head>
+ <body>
+ <form action="{{ request.path }}" method="post">
+ <div>{{ _("Username") }} <input type="text" name="username"/></div>
+ <div>{{ _("Password") }} <input type="password" name="password"/></div>
+ <div><input type="submit" value="{{ _("Sign in") }}"/></div>
+ {{ xsrf_form_html() }}
+ </form>
+ </body>
+ </html>
+
+By default, we detect the user's locale using the `Accept-Language` header
+sent by the user's browser. We choose `en_US` if we can't find an appropriate
+`Accept-Language` value. If you let user's set their locale as a preference,
+you can override this default locale selection by overriding `get_user_locale`
+in your request handler:
+
+ class BaseHandler(tornado.web.RequestHandler):
+ def get_current_user(self):
+ user_id = self.get_secure_cookie("user")
+ if not user_id: return None
+ return self.backend.get_user_by_id(user_id)
+
+ def get_user_locale(self):
+ if "locale" not in self.current_user.prefs:
+ # Use the Accept-Language header
+ return None
+ return self.current_user.prefs["locale"]
+
+If `get_user_locale` returns `None`, we fall back on the `Accept-Language`
+header.
+
+You can load all the translations for your application using the
+`tornado.locale.load_translations` method. It takes in the name of the
+directory which should contain CSV files named after the locales whose
+translations they contain, e.g., `es_GT.csv` or `fr_CA.csv`. The method
+loads all the translations from those CSV files and infers the list of
+supported locales based on the presence of each CSV file. You typically
+call this method once in the `main()` method of your server:
+
+ def main():
+ tornado.locale.load_translations(
+ os.path.join(os.path.dirname(__file__), "translations"))
+ start_server()
+
+You can get the list of supported locales in your application with
+`tornado.locale.get_supported_locales()`. The user's locale is chosen to
+be the closest match based on the supported locales. For example, if the
+user's locale is `es_GT`, and the `es` locale is supported, `self.locale`
+will be `es` for that request. We fall back on `en_US` if no close match
+can be found.
+
+See the [`locale` module](http://github.com/facebook/tornado/blob/master/tornado/locale.py) documentation for detailed information
+on the CSV format and other localization methods.
+
+
+### UI modules
+
+Tornado supports *UI modules* to make it easy to support standard, reusable
+UI widgets across your application. UI modules are like special functional
+calls to render components of your page, and they can come packaged with
+their own CSS and JavaScript.
+
+For example, if you are implementing a blog, and you want to have
+blog entries appear on both the blog home page and on each blog entry page,
+you can make an `Entry` module to render them on both pages. First, create
+a Python module for your UI modules, e.g., `uimodules.py`:
+
+ class Entry(tornado.web.UIModule):
+ def render(self, entry, show_comments=False):
+ return self.render_string(
+ "module-entry.html", show_comments=show_comments)
+
+Tell Tornado to use `uimodules.py` using the `ui_modules` setting in your
+application:
+
+ class HomeHandler(tornado.web.RequestHandler):
+ def get(self):
+ entries = self.db.query("SELECT * FROM entries ORDER BY date DESC")
+ self.render("home.html", entries=entries)
+
+ class EntryHandler(tornado.web.RequestHandler):
+ def get(self, entry_id):
+ entry = self.db.get("SELECT * FROM entries WHERE id = %s", entry_id)
+ if not entry: raise tornado.web.HTTPError(404)
+ self.render("entry.html", entry=entry)
+
+ settings = {
+ "ui_modules": uimodules,
+ }
+ application = tornado.web.Application([
+ (r"/", HomeHandler),
+ (r"/entry/([0-9]+)", EntryHandler),
+ ], **settings)
+
+Within `home.html`, you reference the `Entry` module rather than printing
+the HTML directly:
+
+ {% for entry in entries %}
+ {{ modules.Entry(entry) }}
+ {% end %}
+
+Within `entry.html`, you reference the `Entry` module with the
+`show_comments` argument to show the expanded form of the entry:
+
+ {{ modules.Entry(entry, show_comments=True) }}
+
+Modules can include custom CSS and JavaScript functions by overriding
+the `embedded_css`, `embedded_javascript`, `javascript_files`, or
+`css_files` methods:
+
+ class Entry(tornado.web.UIModule):
+ def embedded_css(self):
+ return ".entry { margin-bottom: 1em; }"
+
+ def render(self, entry, show_comments=False):
+ return self.render_string(
+ "module-entry.html", show_comments=show_comments)
+
+Module CSS and JavaScript will be included once no matter how many times
+a module is used on a page. CSS is always included in the `<head>` of the
+page, and JavaScript is always included just before the `</body>` tag
+at the end of the page.
+
+
+### Non-blocking, asynchronous requests
+
+When a request handler is executed, the request is automatically finished.
+Since Tornado uses a non-blocking I/O style, you can override this default
+behavior if you want a request to remain open after the main request handler
+method returns using the `tornado.web.asynchronous` decorator.
+
+When you use this decorator, it is your responsibility to call
+`self.finish()` to finish the HTTP request, or the user's browser
+will simply hang:
+
+ class MainHandler(tornado.web.RequestHandler):
+ @tornado.web.asynchronous
+ def get(self):
+ self.write("Hello, world")
+ self.finish()
+
+Here is a real example that makes a call to the FriendFeed API using
+Tornado's built-in asynchronous HTTP client:
+
+ class MainHandler(tornado.web.RequestHandler):
+ @tornado.web.asynchronous
+ def get(self):
+ http = tornado.httpclient.AsyncHTTPClient()
+ http.fetch("http://friendfeed-api.com/v2/feed/bret",
+ callback=self.async_callback(self.on_response))
+
+ def on_response(self, response):
+ if response.error: raise tornado.web.HTTPError(500)
+ json = tornado.escape.json_decode(response.body)
+ self.write("Fetched " + str(len(json["entries"])) + " entries "
+ "from the FriendFeed API")
+ self.finish()
+
+When `get()` returns, the request has not finished. When the HTTP client
+eventually calls `on_response()`, the request is still open, and the response
+is finally flushed to the client with the call to `self.finish()`.
+
+If you make calls to asynchronous library functions that require a callback
+(like the HTTP `fetch` function above), you should always wrap your
+callbacks with `self.async_callback`. This simple wrapper ensures that if
+your callback function raises an exception or has a programming error,
+a proper HTTP error response will be sent to the browser, and the connection
+will be properly closed.
+
+For a more advanced asynchronous example, take a look at the `chat` example
+application, which implements an AJAX chat room using
+[long polling](http://en.wikipedia.org/wiki/Push_technology#Long_polling).
+
+
+### Third party authentication
+
+Tornado's `auth` module implements the authentication and authorization
+protocols for a number of the most popular sites on the web, including
+Google/Gmail, Facebook, Twitter, Yahoo, and FriendFeed. The module includes
+methods to log users in via these sites and, where applicable, methods to
+authorize access to the service so you can, e.g., download a user's address
+book or publish a Twitter message on their behalf.
+
+Here is an example handler that uses Google for authentication, saving
+the Google credentials in a cookie for later access:
+
+ class GoogleHandler(tornado.web.RequestHandler, tornado.auth.GoogleMixin):
+ @tornado.web.asynchronous
+ def get(self):
+ if self.get_argument("openid.mode", None):
+ self.get_authenticated_user(self.async_callback(self._on_auth))
+ return
+ self.authenticate_redirect()
+
+ def _on_auth(self, user):
+ if not user:
+ self.authenticate_redirect()
+ return
+ # Save the user with, e.g., set_secure_cookie()
+
+See the `auth` module documentation for more details.
+
+
+Performance
+-----------
+Web application performance is generally bound by architecture, not frontend
+performance. That said, Tornado is pretty fast relative to most popular
+Python web frameworks.
+
+We ran a few remedial load tests on a simple "Hello, world" application
+in each of the most popular Python web frameworks
+([Django](http://www.djangoproject.com/), [web.py](http://webpy.org/), and
+[CherryPy](http://www.cherrypy.org/)) to get the baseline performance of
+each relative to Tornado. We used Apache/mod_wsgi for Django and web.py
+and ran CherryPy as a standalone server, which was our impression of how
+each framework is typically run in production environments. We ran 4
+single-threaded Tornado frontends behind an [nginx](http://nginx.net/)
+reverse proxy, which is how we recommend running Tornado in production
+(our load test machine had four cores, and we recommend 1 frontend per
+core).
+
+We load tested each with Apache Benchmark (`ab`) on the a separate machine
+with the command
+
+ ab -n 100000 -c 25 http://10.0.1.x/
+
+The results (requests per second) on a 2.4GHz AMD Opteron processor with
+4 cores:
+
+<div style="text-align:center;margin-top:2em;margin-bottom:2em"><img src="http://chart.apis.google.com/chart?chxt=y&chd=t%3A100%2C40%2C27%2C25%2C9&chco=609bcc&chm=t+8213%2C000000%2C0%2C0%2C11%7Ct+3353%2C000000%2C0%2C1%2C11%7Ct+2223%2C000000%2C0%2C2%2C11%7Ct+2066%2C000000%2C0%2C3%2C11%7Ct+785%2C000000%2C0%2C4%2C11&chs=600x175&cht=bhs&chtt=Web+server+requests%2Fsec+%28AMD+Opteron%2C+2.4GHz%2C+4+cores%29&chxl=0%3A%7CCherryPy+%28standalone%29%7Cweb.py+%28Apache%2Fmod_wsgi%29%7CDjango+%28Apache%2Fmod_wsgi%29%7CTornado+%281+single-threaded+frontend%29%7CTornado+%28nginx%3B+4+frontends%29%7C"/></div>
+
+In our tests, Tornado consistently had 4X the throughput of the next fastest
+framework, and even a single standalone Tornado frontend got 33% more
+throughput even though it only used one of the four cores.
+
+Not very scientific, but at a high level, it should give you a sense that we
+have cared about performance as we built Tornado, and it shouldn't add too
+much latency to your apps relative to most Python web development frameworks.
+
+
+Running Tornado in production
+-----------------------------
+At FriendFeed, we use [nginx](http://nginx.net/) as a load balancer
+and static file server. We run multiple instances of the Tornado web
+server on multiple frontend machines. We typically run one Tornado frontend
+per core on the machine (sometimes more depending on utilization).
+
+This is a barebones nginx config file that is structurally similar to the
+one we use at FriendFeed. It assumes nginx and the Tornado servers
+are running on the same machine, and the four Tornado servers
+are running on ports 8000 - 8003:
+
+ user nginx;
+ worker_processes 1;
+
+ error_log /var/log/nginx/error.log;
+ pid /var/run/nginx.pid;
+
+ events {
+ worker_connections 1024;
+ use epoll;
+ }
+
+ http {
+ # Enumerate all the Tornado servers here
+ upstream frontends {
+ server 127.0.0.1:8000;
+ server 127.0.0.1:8001;
+ server 127.0.0.1:8002;
+ server 127.0.0.1:8003;
+ }
+
+ include /etc/nginx/mime.types;
+ default_type application/octet-stream;
+
+ access_log /var/log/nginx/access.log;
+
+ keepalive_timeout 65;
+ proxy_read_timeout 200;
+ sendfile on;
+ tcp_nopush on;
+ tcp_nodelay on;
+ gzip on;
+ gzip_min_length 1000;
+ gzip_proxied any;
+ gzip_types text/plain text/html text/css text/xml
+ application/x-javascript application/xml
+ application/atom+xml text/javascript;
+
+ # Only retry if there was a communication error, not a timeout
+ # on the Tornado server (to avoid propagating "queries of death"
+ # to all frontends)
+ proxy_next_upstream error;
+
+ server {
+ listen 80;
+
+ # Allow file uploads
+ client_max_body_size 50M;
+
+ location ^~ /static/ {
+ root /var/www;
+ if ($query_string) {
+ expires max;
+ }
+ }
+ location = /favicon.ico {
+ rewrite (.*) /static/favicon.ico;
+ }
+ location = /robots.txt {
+ rewrite (.*) /static/robots.txt;
+ }
+
+ location / {
+ proxy_pass_header Server;
+ proxy_set_header Host $http_host;
+ proxy_redirect false;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Scheme $scheme;
+ proxy_pass http://frontends;
+ }
+ }
+ }
+
+
+WSGI and Google AppEngine
+-------------------------
+Tornado comes with limited support for [WSGI](http://wsgi.org/). However,
+since WSGI does not support non-blocking requests, you cannot use any
+of the asynchronous/non-blocking features of Tornado in your application
+if you choose to use WSGI instead of Tornado's HTTP server. Some of the
+features that are not available in WSGI applications:
+`@tornado.web.asynchronous`, the `httpclient` module, and the `auth` module.
+
+You can create a valid WSGI application from your Tornado request handlers
+by using `WSGIApplication` in the `wsgi` module instead of using
+`tornado.web.Application`. Here is an example that uses the built-in WSGI
+`CGIHandler` to make a valid
+[Google AppEngine](http://code.google.com/appengine/) application:
+
+ import tornado.web
+ import tornado.wsgi
+ import wsgiref.handlers
+
+ class MainHandler(tornado.web.RequestHandler):
+ def get(self):
+ self.write("Hello, world")
+
+ if __name__ == "__main__":
+ application = tornado.wsgi.WSGIApplication([
+ (r"/", MainHandler),
+ ])
+ wsgiref.handlers.CGIHandler().run(application)
+
+See the `appengine` example application for a full-featured AppEngine
+app built on Tornado.
+
+
+Caveats and support
+-------------------
+Tornado was refactored from the [FriendFeed](http://friendfeed.com/)
+code base to reduce dependencies. This refactoring may have introduced
+bugs. Likewise, because the FriendFeed servers have always run
+[behind nginx](#running-tornado-in-production), Tornado has not been
+extensively tested with HTTP/1.1 clients beyond Firefox. Tornado
+currently does not attempt to handle multi-line headers and some types
+of malformed input.
+
+You can discuss Tornado and report bugs on [the Tornado developer mailing list](http://groups.google.com/group/python-tornado).
diff --git a/vendor/tornado/website/templates/index.html b/vendor/tornado/website/templates/index.html
new file mode 100644
index 0000000000..4aa716598b
--- /dev/null
+++ b/vendor/tornado/website/templates/index.html
@@ -0,0 +1,51 @@
+{% extends "base.html" %}
+
+{% block body %}
+ <p><a href="http://www.tornadoweb.org/">Tornado</a> is an open source version of the scalable, non-blocking web server and tools that power <a href="http://friendfeed.com/">FriendFeed</a>. The FriendFeed application is written using a web framework that looks a bit like <a href="http://webpy.org/">web.py</a> or <a href="http://code.google.com/appengine/docs/python/tools/webapp/">Google's webapp</a>, but with additional tools and optimizations to take advantage of the underlying non-blocking infrastructure.</p>
+ <p>The framework is distinct from most mainstream web server frameworks (and certainly most Python frameworks) because it is non-blocking and reasonably fast. Because it is non-blocking and uses <a href="http://www.kernel.org/doc/man-pages/online/pages/man4/epoll.4.html">epoll</a>, it can handle thousands of simultaneous standing connections, which means it is ideal for real-time web services. We built the web server specifically to handle FriendFeed's real-time features &mdash; every active user of FriendFeed maintains an open connection to the FriendFeed servers. (For more information on scaling servers to support thousands of clients, see The <a href="http://www.kegel.com/c10k.html">C10K problem</a>.)</p>
+ <p>See the <a href="/documentation">Tornado documentation</a> for a detailed walkthrough of the framework.</p>
+
+ <h2>Download and install</h2>
+ <p><b>Download:</b> <a href="/static/tornado-0.2.tar.gz">tornado-0.2.tar.gz</a></p>
+ <pre><code>tar xvzf tornado-0.2.tar.gz
+cd tornado-0.2
+python setup.py build
+sudo python setup.py install</code></pre>
+ <p>The Tornado source code is <a href="http://github.com/facebook/tornado">hosted on GitHub</a>.</p>
+
+ <h3>Prerequisites</h3>
+ <p>Tornado has been tested on Python 2.5 and 2.6. To use all of the features of Tornado, you need to have <a href="http://pycurl.sourceforge.net/">PycURL</a> and a JSON library like <a href="http://pypi.python.org/pypi/simplejson/">simplejson</a> installed. Complete installation instructions for Mac OS X and Ubuntu are included below for convenience.</p>
+ <p style="font-weight:bold">Mac OS X 10.5/10.6</p>
+ <pre><code>sudo easy_install setuptools pycurl==7.16.2.1 simplejson</code></pre>
+
+ <p style="font-weight:bold">Ubuntu Linux</p>
+ <pre><code>sudo apt-get install python-dev python-pycurl python-simplejson</code></pre>
+
+ <h2>Hello, world</h2>
+ <p>Here is the canonical &quot;Hello, world&quot; example app for Tornado:</p>
+ <pre><code>import tornado.httpserver
+import tornado.ioloop
+import tornado.web
+
+class MainHandler(tornado.web.RequestHandler):
+ def get(self):
+ self.write("Hello, world")
+
+application = tornado.web.Application([
+ (r"/", MainHandler),
+])
+
+if __name__ == "__main__":
+ http_server = tornado.httpserver.HTTPServer(application)
+ http_server.listen(8888)
+ tornado.ioloop.IOLoop.instance().start()</code></pre>
+ <p>See the <a href="/documentation">Tornado documentation</a> for a detailed walkthrough of the framework.</p>
+
+ <h2>Discussion and support</h2>
+ <p>You can discuss Tornado and report bugs on <a href="http://groups.google.com/group/python-tornado">the Tornado developer mailing list</a>.
+
+ <h2>Updates</h2>
+ <p>Follow us on <a href="http://www.facebook.com/pages/Tornado-Web-Server/144144048921">Facebook</a>, <a href="http://twitter.com/tornadoweb">Twitter</a>, or <a href="http://friendfeed.com/tornado-web">FriendFeed</a> to get updates and announcements:</p>
+ <div style="margin-top:1em"><a href="http://www.facebook.com/pages/Tornado-Web-Server/144144048921" style="margin-right:10px"><img src="/static/facebook.png" style="width:64px;height:64px" alt="Facebook"/></a><a href="http://twitter.com/tornadoweb" style="margin-right:10px"><img src="/static/twitter.png" style="width:64px;height:64px" alt="Twitter"/></a><a href="http://friendfeed.com/tornado-web" style="margin-right:10px"><img src="/static/friendfeed.png" style="width:64px;height:64px" alt="Facebook"/></a></div>
+
+{% end %}
diff --git a/vendor/tornado/website/website.py b/vendor/tornado/website/website.py
new file mode 100644
index 0000000000..f073b67e6b
--- /dev/null
+++ b/vendor/tornado/website/website.py
@@ -0,0 +1,63 @@
+#!/usr/bin/env python
+#
+# Copyright 2009 Bret Taylor
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import markdown
+import os
+import os.path
+import time
+import tornado.web
+import tornado.wsgi
+import wsgiref.handlers
+
+
+class ContentHandler(tornado.web.RequestHandler):
+ def get(self, path):
+ paths = ("documentation", "index")
+ if not path: path = "index"
+ if path not in paths:
+ raise tornado.web.HTTPError(404)
+ self.render(path + ".html", markdown=self.markdown)
+
+ def markdown(self, path, toc=False):
+ if not hasattr(ContentHandler, "_md") or self.settings.get("debug"):
+ ContentHandler._md = {}
+ if path not in ContentHandler._md:
+ full_path = os.path.join(self.settings["template_path"], path)
+ f = open(full_path, "r")
+ contents = f.read().decode("utf-8")
+ f.close()
+ if toc: contents = u"[TOC]\n\n" + contents
+ md = markdown.Markdown(extensions=["toc"] if toc else [])
+ ContentHandler._md[path] = md.convert(contents).encode("utf-8")
+ return ContentHandler._md[path]
+
+
+settings = {
+ "template_path": os.path.join(os.path.dirname(__file__), "templates"),
+ "xsrf_cookies": True,
+ "debug": os.environ.get("SERVER_SOFTWARE", "").startswith("Development/"),
+}
+application = tornado.wsgi.WSGIApplication([
+ (r"/([a-z]*)", ContentHandler),
+], **settings)
+
+
+def main():
+ wsgiref.handlers.CGIHandler().run(application)
+
+
+if __name__ == "__main__":
+ main()