Commits

David Chambers committed 3731ea5 Merge
  • Participants
  • Parent commits 00195ef, 09300f3
  • Tags 0.8

Comments (0)

Files changed (77)

-VERSION = '0.8dev'
+VERSION = '0.8'

decorators.py

-from django.utils.encoding import smart_unicode
-
-def baseurl():
-    def _(): pass
-    _.cache = None
-    def baseurl(view=None):
-        if view is None:
-            return _.cache
-        def wrapper(request, *args, **kwargs):
-            if _.cache is None:
-                _.cache = smart_unicode(request.build_absolute_uri('/'))
-            return view(request, *args, **kwargs)
-        return wrapper
-    return baseurl
-baseurl = baseurl()

extras/blogger.py

 import mango.settings
 from mango.utils import lstrip, posts_directory
 
+html2text.BODY_WIDTH = mango.settings.BODY_WIDTH
+
 TZ_OFFSET = re.compile(r'(?P<sign>[+-])(?P<hours>\d\d):?(?P<minutes>\d\d)$')
 LINE_BREAKS = re.compile(r'\r\n?|<br( ?/)?>')
 BLOGGER_POST_FOOTER = re.compile(r'<div class="blogger-post-footer">.*?</div>')
 data = connection.read()
 connection.close()
 
-posts_directory() # create posts directory if necessary
+convert = lambda html: html2text.html2text(html).strip() + u'\n'
+
+posts_directory()  # create posts directory if necessary
 
 entries = simplejson.loads(data)['feed']['entry']
 
 for entry in entries:
 
-    lines = []
+    text = u''
 
     timestamp = entry['published']['$t']
 
 
     dt = datetime.strptime(timestamp[:19], '%Y-%m-%dT%H:%M:%S')
     dt = dt.replace(tzinfo=TZ(offset)).astimezone(pytz.timezone(TIME_ZONE))
-    lines.append(u'date: %s' % lstrip(dt.strftime(mango.settings.MARKDOWN_DATE_FORMAT)))
-    lines.append(u'time: %s' % lstrip(dt.strftime(mango.settings.MARKDOWN_TIME_FORMAT)).lower())
-    lines.append(u'zone: %s' % TIME_ZONE)
-    lines.append(u'author: %s' % entry['author'][0]['name']['$t'])
+    text += u'date: %s\n' % lstrip(dt.strftime(mango.settings.MARKDOWN_DATE_FORMAT))
+    text += u'time: %s\n' % lstrip(dt.strftime(mango.settings.MARKDOWN_TIME_FORMAT)).lower()
+    text += u'zone: %s\n' % TIME_ZONE
+    text += u'author: %s\n' % entry['author'][0]['name']['$t']
 
     tags = entry.get('category')
     if tags:
         tags = sorted([smart_unicode(tag['term']) for tag in tags], key=unicode.lower)
-        lines.append(u'tags: %s' % ', '.join(tags))
+        text += u'tags: %s\n' % ', '.join(tags)
 
-    title = entry['title']['$t']
-    lines += ['', '', title, '=' * len(title), '']
+    text += u'\n\n# %s\n\n' % entry['title']['$t']
 
     # tidy up Blogger's mess
     content = re.sub(LINE_BREAKS, '\n', entry['content']['$t'])
     content = re.sub(BLOGGER_POST_FOOTER, '', content)
 
-    # convert HTML to Markdown
-    html2text.BODY_WIDTH = mango.settings.BODY_WIDTH
-    content = html2text.html2text(content).strip()
-
-    lines.append(content)
+    text += convert(content)
 
     for link in entry['link']:
         if link['rel'] == 'alternate':
             if slug.endswith('.html'):
                 slug = slug[:-5]
             break
-    else: # create slug from post title
+    else:  # create slug from post title
         slug = re.sub(r'[^a-z0-9_-]', '', title.lower())
         slug = re.sub(r'\s+', '-', slug)
 
-    with open(os.path.join(mango.settings.DOCUMENTS_PATH, '%s.text' % slug), 'w') as f:
-        f.write(u'%s\n' % '\n'.join(lines))
+    path = os.path.join(mango.settings.DOCUMENTS_PATH, slug + '.text')
+    with open(path, 'w') as f:
+        f.write(text)
 
-sys.stdout.write('Successfully imported %s %s.\n' % (
-        len(entries), len(entries) == 1 and 'document' or 'documents'))
+sys.stdout.write('Successfully imported %s %s.\n' %
+                 (len(entries), len(entries) == 1 and 'document' or 'documents'))
 sys.exit()
 
 import os
 import sys
-import urlparse
-
-try:
-    url = sys.argv[1]
-except IndexError:
-    sys.stderr.write('You must provide the URL at which your Mango site is mounted.\n')
-    sys.exit(1)
 
 try:
     from mercurial import commands, hg, ui
     sys.stderr.write('The mercurial module is missing. Run `pip install mercurial`.\n')
     sys.exit(1)
 
-from django.core import urlresolvers
-from django.core.urlresolvers import reverse
-
 head, tail = os.path.split(os.path.abspath(__file__))
 while tail:
     if tail == 'mango':
 
 Index().uncache(uncache_documents=True)
 
-# monkey patch functions so that URLs can be determined
-
-o = urlparse.urlparse(url)
-
-baseurl = '%s://%s/' % (o.scheme, o.netloc)
-mango.decorators.baseurl = lambda: baseurl
-
-urlpath = o.path
-if urlpath.endswith('/'):
-    urlpath = urlpath[:-1]
-urlresolvers.reverse = lambda lookup_view: '%s%s' % (urlpath, reverse(lookup_view))
-
 # create and cache documents
 
 Index.get()
 import mango.settings
 from mango.utils import lstrip, posts_directory, unescape
 
+html2text.BODY_WIDTH = mango.settings.BODY_WIDTH
+delim = u' '.join(['*'] * int(round(html2text.BODY_WIDTH / 2))) + u'\n'
+
 # assign arguments to variables
 try:
     blog_url, username, password = sys.argv[1:]
     sys.stderr.write('Failed to find blog. Double-check supplied arguments.\n')
     sys.exit(1)
 
-posts_directory() # create posts directory if necessary
+convert = lambda html: html2text.html2text(html).strip() + u'\n'
+
+posts_directory()  # create posts directory if necessary
 
 pages = server.wp.getPages(blog_id, username, password)
 
 for page in pages:
-    html2text.BODY_WIDTH = mango.settings.BODY_WIDTH
     with open(os.path.join(mango.settings.DOCUMENTS_PATH, '%s.text' % page['wp_slug']), 'w') as f:
-        f.write(u'author: %s\n\n\n%s\n%s\n\n%s\n' % (
-                page['wp_author_display_name'],
-                page['title'], '=' * len(page['title']),
-                html2text.html2text(page['description']).strip()))
+        f.write(u'author: %s\n\n\n# %s\n\n%s' %
+                (page['wp_author_display_name'], page['title'],
+                 convert(page['description'])))
 
 posts = server.metaWeblog.getRecentPosts(blog_id, username, password, 0)
 
 for post in posts:
 
-    lines = []
+    text = u''
 
     dt = datetime.strptime(post['date_created_gmt'].value, '%Y%m%dT%H:%M:%S')
     dt = dt.replace(tzinfo=pytz.utc).astimezone(pytz.timezone(TIME_ZONE))
-    lines.append(u'date: %s' % lstrip(dt.strftime(mango.settings.MARKDOWN_DATE_FORMAT)))
-    lines.append(u'time: %s' % lstrip(dt.strftime(mango.settings.MARKDOWN_TIME_FORMAT)).lower())
-    lines.append(u'zone: %s' % TIME_ZONE)
-    lines.append(u'author: %s' % post['wp_author_display_name'])
+    text += u'date: %s\n' % lstrip(dt.strftime(mango.settings.MARKDOWN_DATE_FORMAT))
+    text += u'time: %s\n' % lstrip(dt.strftime(mango.settings.MARKDOWN_TIME_FORMAT)).lower()
+    text += u'zone: %s\n' % TIME_ZONE
+    text += u'author: %s\n' % post['wp_author_display_name']
 
     if post['mt_keywords']:
-        lines.append(u'tags: %s' % unescape(post['mt_keywords']))
+        text += u'tags: %s' % unescape(post['mt_keywords'])
 
-    if post['mt_excerpt']: # hand-crafted excerpt
-        lines += ['', '']
-        html2text.BODY_WIDTH = mango.settings.BODY_WIDTH - 2
-        for line in html2text.html2text(post['mt_excerpt']).strip().splitlines():
-            lines.append(u'| %s' % line)
+    if post['mt_excerpt']:  # hand-crafted excerpt
+        text += u'\n\n%s' % convert(post['mt_excerpt'])
 
-    lines += ['', '', post['title'], '=' * len(post['title']), '']
+    text += u'\n\n# %s\n\n' % post['title']
 
-    html2text.BODY_WIDTH = mango.settings.BODY_WIDTH
-    lines.append(html2text.html2text(post['description']).strip())
+    # everything before the <!--more-->, or the entire post body
+    excerpt = convert(post['description'])
+    # everything after the <!--more-->, or nothing
+    rest = post['mt_text_more']
+    text += u'\n'.join([delim, excerpt, delim, convert(rest)]) if rest else excerpt
 
-    if post['mt_text_more']: # <!--more-->
-        lines.append('')
-        html2text.BODY_WIDTH = mango.settings.BODY_WIDTH - 2
-        for line in html2text.html2text(post['mt_text_more']).strip().splitlines():
-            lines.append(u'| %s' % line)
+    path = os.path.join(mango.settings.DOCUMENTS_PATH, post['wp_slug'] + '.text')
+    with open(path, 'w') as f:
+        f.write(text)
 
-    with open(os.path.join(mango.settings.DOCUMENTS_PATH, '%s.text' % post['wp_slug']), 'w') as f:
-        f.write(u'%s\n' % '\n'.join(lines))
-
-sys.stdout.write('Successfully imported %s %s and %s %s.\n' % (
-        len(pages), len(pages) == 1 and 'page' or 'pages',
-        len(posts), len(posts) == 1 and 'post' or 'posts'))
+sys.stdout.write('Successfully imported %s %s and %s %s.\n' %
+                 (len(pages), len(pages) == 1 and 'page' or 'pages',
+                  len(posts), len(posts) == 1 and 'post' or 'posts'))
 sys.exit()
 from django.utils import feedgenerator
 
 import mango.settings
-from mango.decorators import baseurl
 from mango.main import Index
 
-@baseurl
 def atom(request, tag=None):
     content_type = 'application/atom+xml; charset=utf-8'
     feed_url = request.build_absolute_uri()
         for key, function in optional.items():
             try:
                 value = function()
-                if value: # don't include pair if its value is empty
+                if value:  # don't include pair if its value is empty
                     kwargs[key] = value
             except KeyError:
                 pass
 
     required = {
         'title':        lambda: mango.settings.SITE_TITLE,
-        'link':         lambda: baseurl(),
-        'description':  lambda: u'', # required by constructor, but does not affect output
+        'link':         lambda: mango.settings.BASE_URL,
+        'description':  lambda: u'',  # required by constructor, but does not affect output
     }
     optional = {
         'author_name':  lambda: mango.settings.PRIMARY_AUTHOR_NAME,
         'author_email': lambda: mango.settings.PRIMARY_AUTHOR_EMAIL,
         'author_link':  lambda: mango.settings.PRIMARY_AUTHOR_URL,
         'feed_url':     lambda: feed_url,
-        'feed_guid':    lambda: baseurl(),
+        'feed_guid':    lambda: mango.settings.BASE_URL,
     }
 
     kwargs = all_kwargs(required, optional)
 from django import forms
 
 import mango.settings
-from mango.decorators import baseurl
-from mango.utils import akismet_request
+from mango.utils import akismet_request, normalizelinebreaks
 
 
 class CommentForm(forms.Form):
 
     author_name = forms.CharField(label='Name', initial='name', max_length=100)
-    author_email = forms.EmailField(label='E-mail', initial='e-mail address', max_length=100)
-    author_url = forms.URLField(label='Website', initial='Web address (optional)', max_length=100, required=False)
+    author_email = forms.EmailField(label='E-mail', initial='e-mail address',
+                                    max_length=100)
+    author_url = forms.URLField(label='Website',
+                                initial='Web address (optional)',
+                                max_length=100, required=False)
     message = forms.CharField(label='Comment', widget=forms.Textarea)
-    subscribe = forms.BooleanField(label='Notify me of follow-up comments via e-mail', required=False)
+    subscribe = forms.BooleanField(label=('Notify me of follow-up '
+                                          'comments via e-mail'),
+                                   required=False)
 
     def __init__(self, *args, **kwargs):
         self.request = kwargs.pop('request', None)
         return self.cleaned_data.get('author_url', '').strip()
 
     def clean_message(self):
-        return self.cleaned_data.get('message', '').strip().replace('\r\n', '\n')
+        message = self.cleaned_data.get('message', '').strip()
+        return normalizelinebreaks(message)
 
     def clean(self):
         if mango.settings.AKISMET_API_KEY and self.is_valid():
             args = {
-                'blog': baseurl(),
+                'blog': mango.settings.BASE_URL,
                 'user_ip': self.request.META['REMOTE_ADDR'],
                 'user_agent': self.request.META['HTTP_USER_AGENT'],
                 'referrer': self.request.META['HTTP_REFERER'],
 
 class ContactForm(forms.Form):
 
-    sender_name = forms.CharField(label='Name', initial='your name', max_length=100)
-    sender_email = forms.EmailField(label='E-mail', initial='your e-mail address', max_length=100)
-    subject = forms.CharField(label='Subject', initial='subject', max_length=100, required=False)
+    sender_name = forms.CharField(label='Name', initial='your name',
+                                  max_length=100)
+    sender_email = forms.EmailField(label='E-mail',
+                                    initial='your e-mail address',
+                                    max_length=100)
+    subject = forms.CharField(label='Subject', initial='subject',
+                              max_length=100, required=False)
     message = forms.CharField(label='Message', widget=forms.Textarea)
-    cc_sender = forms.BooleanField(label='Send me a copy of this message', required=False)
+    cc_sender = forms.BooleanField(label='Send me a copy of this message',
+                                   required=False)
 
     def clean_sender_name(self):
         return self.cleaned_data.get('sender_name', '').strip()
         return self.cleaned_data.get('subject', '').strip()
 
     def clean_message(self):
-        return self.cleaned_data.get('message', '').strip().replace('\r\n', '\n')
+        message = self.cleaned_data.get('message', '').strip()
+        return normalizelinebreaks(message)
 from django.template import Context, loader
 
 import mango.settings
-from mango.decorators import baseurl
 from mango.main import Index
 from mango.utils import logger, text_response
 if mango.settings.SUBSCRIPTIONS:
-    from mango.models import Subscription
+    from mango.models import Subscription, SubscriptionMessage
 
-@baseurl
 def flush_cache(request):
     Index().uncache()
     return text_response('Top-level cache flushed.')
 
-@baseurl
 def moderate(request, action):
     from mango import disqus
 
         cache.delete(cache_key)
         logger.debug('Cache key invalidated: %s' % cache_key)
 
+        try:
+            SubscriptionMessage.objects.get(url=thread_url, comment_id=post_id)
+        except SubscriptionMessage.DoesNotExist:
+            pass
+        else:
+            return text_response('Comment approved.')
+
         if mango.settings.SUBSCRIPTIONS and thread_url: # notify subscribers
             subject = u'New comment on "%s"' % comment.thread.title
-            text_template = loader.get_template('email/subscriber.dtext')
-            html_template = loader.get_template('email/subscriber.dhtml')
+            text_template = loader.get_template('email/subscriber.text')
+            html_template = loader.get_template('email/subscriber.html')
 
             for sub in Subscription.objects.filter(url=thread_url):
-                context = Context({'comment': comment, 'subscription_id': sub.id})
-                msg = EmailMultiAlternatives(subject, text_template.render(context),
-                        to=[u'%s <%s>' % (sub.subscriber_name, sub.subscriber_email)])
-                msg.attach_alternative(html_template.render(context), 'text/html')
+                ctx = Context({'comment': comment, 'subscription_id': sub.id})
+                to = [u'%s <%s>' % (sub.subscriber_name, sub.subscriber_email)]
+                msg = EmailMultiAlternatives(subject,
+                                             text_template.render(ctx), to=to)
+                msg.attach_alternative(html_template.render(ctx), 'text/html')
                 msg.send(fail_silently=False)
 
+            SubscriptionMessage(url=thread_url, comment_id=post_id).save()
+
         return text_response('Comment approved.')
 
     if action == 'spam':
 
     return text_response(message)
 
-@baseurl
 def redirect(request, path, fragment=None):
     url = reverse('mango.views.post', args=(path,))
     return HttpResponseRedirect('%s#%s' % (url, fragment) if fragment else url)
 
-@baseurl
 def unsubscribe(request, path, subscription_id):
     subscription = get_object_or_404(Subscription, pk=subscription_id)
     subscription.delete()
 # -*- coding: utf-8 -*-
 
 from __future__ import with_statement
-import datetime
+from functools import partial
 import math
 import os
 import re
 
+try:
+    from itertools import zip_longest as izip_longest
+except ImportError:
+    from itertools import izip_longest
+
 import lxml.html
-import markdown
-import pytz
+from markdown import Markdown
 
-from django.conf import settings
 from django.core import urlresolvers
 from django.core.cache import cache
 from django.template import Context, loader
-from django.utils.encoding import smart_str, smart_unicode
+from django.utils.encoding import smart_unicode
 from django.utils.safestring import mark_safe
 
 import mango.settings
-from mango import decorators
 from mango.template import Script, StyleSheet
-from mango.template.code import supported_code_sites
-from mango.template.video import supported_video_sites
+from mango.template.embed import embed
 from mango.templatetags.mango import convert_html_chars
-from mango.utils import canonicalize, logger, replace, slugify
+from mango.utils import (logger, nonstringiterable, normalizelinebreaks,
+                         parsedocdate, replace, slugify, validfilenames)
 
 
-_alias_canon_match = re.compile(r'^(0*(?P<alias>.*?)(?P<separator>=>?))?(?P<canon>.+)$').match
+_alias_canon_match = re.compile(r'^(0*(?P<alias>.*?)=)?(?P<canon>.+)$').match
 
 class Resource(object):
     def __init__(self, path):
         while tail:
             match = _alias_canon_match(tail)
             if match:
-                if match.group('separator') == '=>':
-                    logger.warning('"=>" separator is deprecated -- rename document, replacing "=>" with "="')
                 canon = match.group('canon')
                 if not canon_fragments and not os.path.isdir(path):
-                    canon = os.path.splitext(canon)[0] # strip extension
+                    canon = os.path.splitext(canon)[0]  # strip extension
                 canon_fragments.insert(0, canon)
                 short_fragments.insert(0, match.group('alias') or canon)
             head, tail = os.path.split(head)
         mount_point = urlresolvers.reverse('mango.views.index')
         repl = (mango.settings.UNIX_DOCUMENTS_PATH, u'', 1)
-        canon_path = (u'%s%s/' % (mount_point, '/'.join(canon_fragments))).replace(*repl).lower()
-        short_path = (u'%s%s/' % (mount_point, '/'.join(short_fragments))).replace(*repl).lower()
+        canon_path = u'%s%s/' % (mount_point, '/'.join(canon_fragments))
+        canon_path = canon_path.replace(*repl).lower()
+        short_path = u'%s%s/' % (mount_point, '/'.join(short_fragments))
+        short_path = short_path.replace(*repl).lower()
         self.urls = {
             'canon': {
                 'rel': canon_path,
-                'abs': u'%s%s' % (mango.settings.BASE_URL or decorators.baseurl(), canon_path[1:]),
+                'abs': u'%s%s' % (mango.settings.BASE_URL, canon_path[1:]),
             },
             'short': {
                 'rel': short_path,
-                'abs': u'%s%s' % (mango.settings.SHORT_URL_BASE or decorators.baseurl(), short_path[1:]),
+                'abs': u'%s%s' % (mango.settings.SHORT_URL_BASE or
+                                  mango.settings.BASE_URL, short_path[1:]),
             },
         }
 
             # append the canonical fragment to each alias
             updated = [u'/'.join([alias, canon_fragment]) for alias in aliases]
             if short_fragment != canon_fragment:
-                # branch each alias and append the short fragment to the new branches
-                updated += [u'/'.join([alias, short_fragment]) for alias in aliases]
+                # branch each alias and append the short fragment
+                # to the new branches
+                updated += [u'/'.join([alias, short_fragment])
+                            for alias in aliases]
             aliases = updated
         self._aliases = aliases
         self._slug = canon_fragments[-1]
+        self._references = {}
 
     def identifier(self):
         return self.urls['canon']['rel']
         urlpaths = []
         for key in self.meta.get('require', []):
             try:
-                value = mango.settings.STATIC_FILES[key]
+                items = mango.settings.STATIC_FILES[key]
             except KeyError:
-                logger.warning('STATIC_FILES does not contain the key "%s"' % key)
+                logger.warning('STATIC_FILES does not contain the key "%s"'
+                               % key)
             else:
-                for urlpath in [value] if isinstance(value, basestring) else value:
+                for urlpath in nonstringiterable(items):
                     urlpaths.append(urlpath)
         return urlpaths
 
     def scripts(self):
-        return [Script(path) for path in self.required_files() if path.endswith('.js')]
+        return [Script(path) for path in self.required_files()
+                if path.endswith('.js')]
 
     def stylesheets(self):
-        return [StyleSheet(path) for path in self.required_files() if path.endswith('.css')]
+        return [StyleSheet(path) for path in self.required_files()
+                if path.endswith('.css')]
 
+    def _setreferences(self, references):
+        self._references = references.copy()
+    references = property(lambda self: self._references.copy(), _setreferences)
 
-_code_tags_sub = re.compile(r'<code>(.*?)</code>').sub
-_heading_match = re.compile(r'(?m)\s*<(h[1-6])[^>]*>(?P<title>.+?)</\1>$(?P<html>[\s\S]*)').match
+    _subhandcraftedexcerpt = re.compile(
+        r'''^
+        (?:                                 # optional metadata
+          [ ]{,3}[-\w]+:.*\n                # "key: value" pair
+          (?:[ ]{4,}.*\n)*                  # value may wrap
+        )*
+        (?P<excerpt>                        # arbitrary excerpt
+          (?:
+            (?![ ]{,3}(?:\*[ ]?){3,}\s*\n)  # section delimiters not allowed
+            [^\n]*\n
+          )+?
+        )
+        (?= \#(?!\#) | .+\n={3,} )          # followed by an h1 of either type
+        ''', re.VERBOSE).sub
 
-def _pluck_title(resource):
-    title_text = resource.meta.get('title', u'')
-    title = convert_html_chars(title_text)
-    html = resource.html
+    def _pluckhandcraftedexcerpt(self):
+        def repl(match):
+            excerpt = match.group('excerpt')
+            self._excerpt = self._convert(excerpt) + '\n'
+            return ''
+        return self._subhandcraftedexcerpt(repl, self.source)
 
-    if not title_text:
-        match = _heading_match(html)
-        if match:
-            title, html = match.group('title'), match.group('html')
-            # convert `lxml.etree._ElementUnicodeResult` to `unicode` for pickling
-            title_text = unicode(lxml.html.fromstring(_code_tags_sub(r'`\1`', title)).text_content())
+    def _plucktitle():
+        subcodetags = re.compile(r'<code>(.*?)</code>').sub
+        matchheading = re.compile(r'\s*<h1[^>]*>(?P<title>.+?)</h1>$'
+                                  r'(?P<html>[\s\S]*)', re.MULTILINE).match
+        def plucktitle(self):
+            self.title_text = self.meta.get('title', u'')
+            self.title = convert_html_chars(self.title_text)
 
-    return title_text, mark_safe(title), mark_safe(html)
+            if not self.title_text:
+                match = matchheading(self.html)
+                if match:
+                    self.html = match.group('html')
+                    self.title = match.group('title')
+                    self.title_text = subcodetags(r'`\1`', self.title)
+                    self.title_text = lxml.html.fromstring(self.title_text)
+                    # convert `lxml.etree._ElementUnicodeResult` to `unicode`
+                    # for pickling
+                    self.title_text = unicode(self.title_text.text_content())
+            self.title = mark_safe(self.title)
+        return plucktitle
+    _plucktitle = _plucktitle()
 
-def _split_meta_lists():
-    meta = getattr(md, 'Meta', {})
+
+def processmetalists(meta):
     for key, value in meta.items():
-        if len(value) != 1: # note: `value` is always a list
+        if len(value) != 1:  # note: `value` is always a list
             meta[key] = value
         elif key in mango.settings.META_LISTS:
             meta[key] = [item.strip() for item in value[0].split(',')]
             meta[key] = value[0].strip()
     return meta
 
-_invalid_filename = re.compile(r'(^_index$|^[.]|[\a\b\s]|~$)').search
-
 class Category(Resource):
     def __init__(self, dirpath, use_cache=True):
         super(Category, self).__init__(path=dirpath)
         self._dirpath = dirpath
-        self.name = _alias_canon_match(os.path.basename(dirpath)).group('canon')
+        match = _alias_canon_match(os.path.basename(dirpath))
+        self.name = match.group('canon')
         self._pages = []
         self._posts = []
         self.subcategories = []
 
         try:
             with open(os.path.join(dirpath, '_index')) as f:
-                self._contents = smart_unicode(f.read())
+                self.source = smart_unicode(f.read())
         except IOError:
-            self.html = self._contents = None
+            self.html = self.source = None
             self.meta = {}
         else:
             md.reset()
-            self.html = md.convert(self._contents)
-            self.meta = _split_meta_lists()
-            _, title, self.html = _pluck_title(self)
-            if title:
-                self.name = title
+            self.html = md.convert(self.source)
+            self.meta = processmetalists(getattr(md, 'Meta', {}))
+            self._plucktitle()
+            if self.title:
+                self.name = self.title
 
-        for name in [fn for fn in os.listdir(dirpath) if not _invalid_filename(fn)]:
+        for name in validfilenames(os.listdir(dirpath)):
             path = os.path.join(dirpath, name)
             if os.path.isdir(path):
                 self.subcategories.append(Category(path))
             match = subcategory.find_match(urlpath)
             if match:
                 return match
-        return None
 
     def _documents_in_index_order(self, documents, kind):
         if kind not in self.meta:
         tags = []
         for document in self._pages + self._posts:
             if document.tags():
-                tags += document.tags(alphabetical=False) # no point in sorting
+                tags += document.tags(alphabetical=False)  # no need to sort
         for subcategory in self.subcategories:
             tags += subcategory._tags()
         return tags
 class Index(Category):
     @staticmethod
     def cache_key():
-        return 'mango:toplevel:%s' % decorators.baseurl()
+        return 'mango:toplevel:%s' % mango.settings.BASE_URL
 
     @classmethod
     def get(cls):
         return index
 
     def __init__(self, use_cache=False):
-        super(Index, self).__init__(dirpath=mango.settings.DOCUMENTS_PATH, use_cache=use_cache)
+        super(Index, self).__init__(dirpath=mango.settings.DOCUMENTS_PATH,
+                                    use_cache=use_cache)
 
     def uncache(self, uncache_documents=False):
         cache.delete(Index.cache_key())
         return self
 
 
-_match = r'(?m)^(%s(?=[ \n]) ?)'
-_excerpt_pipes_sub = re.compile(_match % r'\|').sub
-_update_pipes_sub = re.compile(_match % r'\|\|').sub
+stripskips = partial(re.compile(r'</?skip>').sub, '')
 
-_meta = r'(?m)^(( {,3})%s:(\s*).*\n)'
-_time = re.compile(_meta % 'time')
-_zone = re.compile(_meta % 'zone')
+md = Markdown(extensions=('meta',) + mango.settings.MARKDOWN_EXTENSIONS)
+_renderupdate = loader.get_template('update.html').render
 
-_update_meta = r'(?m)^(((?:\| )?\|\| {1,4})%s:(\s*).*\n)'
-_update_time = re.compile(_update_meta % 'time')
-_update_zone = re.compile(_update_meta % 'zone')
+def pairer(pattern):
+    fn = re.compile(pattern, re.MULTILINE).split
+    def grouper(*args, **kwargs):
+        return izip_longest(fillvalue='', *([iter(fn(*args, **kwargs))] * 2))
+    return grouper
 
-_excerpt_finditer = re.compile(r'(?m)^(\|(?=[ \n])[^\n]*(\n|$))+').finditer
-_filesize_sub = re.compile(r'''{{\s*filesize:(['"])(?P<filepath>\S+)\1\s*}}''').sub
-_handcrafted_match = re.compile(r'( {,3}\S+:.*(\n[ \t]*\S+:.*)*\n{2,})?(?P<excerpt>(\|(?=[ \n])[^\n]*\n)+)').match
-_line_break_sub = re.compile(r'\r\n?').sub
-_skip_sub = re.compile(r'</?skip>').sub
-_update_split = re.compile(r'(?m)^((?:\|\|(?=[ \n])[^\n]*(?:\n|$))+)').split
-_video_link_sub = re.compile(r'(?m)^<(?P<tag>dt|p)><a href="(?P<url>[^"]+)"(?: title="(?P<title>[^"]*)")?>(?P<text>[^<]+)</a></\1>$').sub
+itersections = pairer(r'([\s\S]+?)'.join([r'^[ ]{,3}(?:\*[ ]?){3,}\s*\n'] * 2))
 
-md = markdown.Markdown(extensions=('meta',) + mango.settings.MARKDOWN_EXTENSIONS)
-update_template = loader.get_template('update.dhtml')
+matchupdate = re.compile(
+    r'''^(?:\s*\n)?             # zero or more empty lines
+    (?P<level>\#{1,6})[ \t]*    # one or more number signs
+    update\b                    # "update" (case insensitive)
+    (?P<metadata>[^\n]+)\n+     # date, time, and time zone
+    (?P<body>.+)$               # the update's body
+    ''',
+    re.DOTALL | re.IGNORECASE | re.VERBOSE).match
+
+_finddate = re.compile(r'\b(?:[1-9]|[12]\d|3[01]) [a-zA-Z]+ \d{4}\b').search
+_findtime = re.compile(r'\b(?:[1-9]|1[0-2]):\d\d[ap]m\b').search
+_findzone = re.compile(r'\b[a-zA-Z_]+(?:/[a-zA-Z_-]+){1,2}\b').search
+
+def renderupdate(resource, match):
+    metadata = match.group('metadata')
+
+    date = _finddate(metadata)
+    time = _findtime(metadata)
+    zone = _findzone(metadata)
+
+    if not date or not time or not zone:
+        return
+
+    date, time, zone = date.group(0), time.group(0), zone.group(0)
+
+    for text in (date, time, zone):
+        metadata = metadata.replace(text, '', 1)
+
+    # Leftover commas, dashes, parentheses, etc. are fine. Bail
+    # in the unlikely event that -- having stripped date, time,
+    # and zone -- `metadata` contains "significant" characters.
+    if re.search(r'\w', metadata):
+        return
+
+    update = Update()
+    try:
+        update.datetime = parsedocdate(date, time, zone)
+    except ValueError:
+        logger.exception('Update date/time/zone incorrectly formatted')
+        return
+
+    update.headinglevel = len(match.group('level'))
+    update.html = mark_safe(resource._convert(match.group('body')))
+
+    return _renderupdate(Context({'update': update}))
 
 class Document(Resource):
     @classmethod
 
         return document
 
-    def __init__(self, filepath=None, parent=None):
-        if parent is None and filepath:
+    def __init__(self, filepath=None):
+        self.source = None
+        if filepath is not None:
             super(Document, self).__init__(path=filepath)
             with open(filepath) as f:
-                self._contents = smart_unicode(f.read())
-        else:
-            self._contents = None
+                self.source = smart_unicode(f.read())
 
         self._filepath = filepath
-        self._parent = parent
         self._tags = None
         self._thread_id = None
 
         self.previous = None
         self.next = None
 
-        if parent is None:
-            self.convert()
+        self.convert()
 
-    def convert(self, contents=None):
-        if contents is None:
-            contents = self._contents
-            if not contents:
-                return self
-        else:
-            self._contents = contents
+    def _convert():
+        sub = re.compile(r'^[ ]{,3}(?:\*[ ]?){3,}[ \t]*$', re.MULTILINE).sub
+        def warn(match):
+            logger.warning('Horizontal rules comprised of '
+                           'asterisks delimit excerpts/updates '
+                           '(use hyphens or underscores instead)')
+            return ''
+        def render(self, text):
+            md.reset()
+            md.references = self.references
+            return md.convert(sub(warn, text))
+        return render
+    _convert = _convert()
+
+    def convert(self, source=None):
+        if source is not None:
+            self.source = source
+
+        if self.source is None:
+            return self
+
+        self.source = normalizelinebreaks(self.source) + '\n'
 
         md.reset()
-        # populate `md.references` with enclosing scopes' link definitions
-        parent = self._parent
-        while parent:
-            if hasattr(parent, '_references'):
-                md.references.update((k, v) for k, v in parent._references.items() if k not in md.references)
-            parent = parent._parent
+        # discern link definitions
+        md.convert(self.source)
+        self.references = md.references
 
-        contents = u'%s\n' % _line_break_sub('\n', contents)
-        if isinstance(self, Update):
-            contents = _update_pipes_sub(u'', contents)
+        self.meta = processmetalists(getattr(md, 'Meta', {}))
 
-        self.body = contents
+        self.html = self._excerpt = ''
 
-        # excerpts
-        snippets = []
-        match = _handcrafted_match(contents)
-        if match:
-            capture = match.group('excerpt')
-            snippets.append(_excerpt_pipes_sub(u'', capture))
-            contents = contents.replace(capture, u'')
-        for match in _excerpt_finditer(contents):
-            capture = match.group(0)
-            snippet = _excerpt_pipes_sub(u'', capture)
-            snippets.append(snippet)
-            contents = contents.replace(capture, snippet)
+        source = self._pluckhandcraftedexcerpt()
 
-        # updates
-        split = _update_split(contents)
-        chunks = md.convert(u'\n<mango/>\n'.join(split[0::2])).split('<mango/>')
-        self._references = md.references.copy() # save current scope's link definitions
-        self.meta = _split_meta_lists()
-        updates = [update_template.render(Context({'update': Update(parent=self).convert(item)})) for item in split[1::2]]
-        combined = [None] * (len(chunks) + len(updates))
-        combined[0::2] = chunks
-        combined[1::2] = updates
-        self.html = u''.join(combined)
+        for nonsection, section in itersections(source):
+            self.html += self._convert(nonsection) + '\n'
+
+            match = matchupdate(section)
+            if match:
+                update = renderupdate(self, match)
+                if update:
+                    self.html += update
+                    continue
+
+            if section:
+                excerpt = self._convert(section) + '\n'
+                self._excerpt += excerpt
+                self.html += excerpt
+
+        self._excerpt = mark_safe(replace(self._excerpt.rstrip('\n')))
 
         if 'tags' in mango.settings.META_LISTS and 'tags' in self.meta:
             self._tags = [Tag(tag) for tag in self.meta['tags']]
 
         author_name = self.meta.get('author')
-        self.author = author_name and {'name': author_name, 'url': mango.settings.AUTHORS.get(author_name)} or None
+        if author_name:
+            self.author = {
+                'name': author_name,
+                'url': mango.settings.AUTHORS.get(author_name),
+            }
 
         self.datetime = None
-        if 'date' in self.meta and 'time' in self.meta:
-            def update_post(pattern, repl):
-                master = self.master()
-                if master._filepath:
-                    with open(master._filepath, 'w') as f:
-                        contents = self._contents
-                        contents = master._contents.replace(contents, re.sub(pattern, repl, contents), 1)
-                        f.write(contents.encode('utf-8'))
-                        master._contents = contents
+        if all(key in self.meta for key in ('date', 'time', 'zone')):
+            try:
+                self.datetime = parsedocdate(self.meta['date'],
+                                             self.meta['time'],
+                                             self.meta['zone'])
+            except ValueError:
+                logger.exception('Document date/time/zone '
+                                 'incorrectly formatted')
 
-            if 'zone' in self.meta:
-                tz = pytz.timezone(smart_str(canonicalize(self.meta['zone'], mango.settings.TIME_ZONES)))
-                if self.meta['zone'] != tz.zone:
-                    pattern = _update_zone if self._parent else _zone
-                    update_post(pattern, r'\2zone:\3%s\n' % tz.zone)
-            else:
-                tz = pytz.timezone(settings.TIME_ZONE)
-                pattern = _update_time if self._parent else _time
-                update_post(pattern, r'\1\2zone:\3%s\n' % tz.zone)
-
-            dt_format = u'%s %s' % (
-                    mango.settings.MARKDOWN_DATE_FORMAT,
-                    mango.settings.MARKDOWN_TIME_FORMAT)
-            try:
-                self.datetime = tz.localize(datetime.datetime.strptime('%s %s' % (
-                        self.meta['date'], self.meta['time']), dt_format)).astimezone(pytz.utc)
-            except ValueError:
-                logger.warning('Date and/or time incorrectly formatted')
-
-        self.title_text, self.title, self.html = _pluck_title(self)
-
-        def embed_media(match):
-            for site in supported_code_sites + supported_video_sites:
-                result = site.render(**match.groupdict())
-                if result:
-                    return result
-            return match.group(0)
-        self.html = _video_link_sub(embed_media, self.html)
-
-        def filesize(filepath, kilobyte_size=mango.settings.KILOBYTE_SIZE):
-            if not os.path.isabs(filepath):
-                filepath = os.path.join(mango.settings.PROJECT_PATH, filepath)
-            try:
-                filesize = os.path.getsize(filepath)
-            except OSError:
-                return u'' # fail silently
-
-            bytes = (
-                ('bytes', 1),
-                ('kB', kilobyte_size**1),
-                ('MB', kilobyte_size**2),
-                ('GB', kilobyte_size**3),
-                ('TB', kilobyte_size**4),
-            )
-            for unit, value in bytes:
-                if filesize <= value * kilobyte_size or unit == 'TB':
-                    if unit == 'bytes':
-                        return u'(%s\u2009bytes)' % filesize
-                    else:
-                        return u'(≈%.1f\u2009%s)' % (float(filesize)/value, unit)
-
-        self.body = _skip_sub('', self.body)
-
-        self.html = mark_safe(
-                _skip_sub('', replace(_filesize_sub(
-                lambda match: u'<span class="filesize">%s</span>' % (
-                filesize(match.group('filepath'))), self.html.lstrip('\n')))))
-
-        if type(self) is Document:
-            self.excerpt = mark_safe('\n'.join([Excerpt(parent=self).convert(s).html for s in snippets])) if snippets else self.html
-            self.type = self.meta.get('type', 'post' if self.datetime else 'page')
-
-        md.reset() # clear cached link definitions
+        self._plucktitle()
+        self.html = embed(self.html.rstrip())
+        self.html = mark_safe(stripskips(replace(self.html.lstrip('\n'))))
+        self.type = self.meta.get('type', 'post' if self.datetime else 'page')
 
         return self
 
         if self._thread_id is not None:
             thread = cache.get(cache_key(self._thread_id))
             if thread is not None:
-                logger.debug('Disqus thread for "%s" retrieved from cache' % title)
+                logger.debug('Disqus thread for "%s" retrieved from cache'
+                             % title)
                 return thread
 
             try:
 
         # In theory, threads could be cached indefinitely since cache keys
         # are invalidated when required in response to comment moderation.
-        cache.set(cache_key(thread.id), thread, 24*60*60)
+        cache.set(cache_key(thread.id), thread, 24 * 60 * 60)
         logger.debug('Disqus thread for "%s" cached' % title)
         self._thread_id = thread.id
         return thread
                 return thread.posts
         return []
 
+    def excerpt(self):
+        return self._excerpt or self.html
+
     def has_tag(self, tag):
         return tag in self.meta.get('tags', [])
 
-    def master(self):
-        master = self
-        parent = self._parent
-        while parent:
-            master = parent
-            parent = parent._parent
-        return master
-
     def related(self, documents=None):
         if documents is None:
             documents = Index.get().descendants()
         related.sort(reverse=True)
         related = [document for similarity, document in related]
         if cache_key:
-            cache.set(cache_key, related, 15*60)
+            cache.set(cache_key, related, 15 * 60)
         return related
 
     def safe_slug(self):
     def uncache(self):
         if self._filepath:
             cache.delete('mango:%s' % self._filepath)
-            logger.debug('Document object purged from cache: %s' % self._filepath)
+            logger.debug('Document object purged from cache: %s'
+                         % self._filepath)
         return self
 
     def __eq__(self, other):
         return self.__unicode__().encode('utf-8')
 
     def __unicode__(self):
-        return getattr(self.master(), 'title', 'Untitled Document')
+        return getattr(self, 'title', 'Untitled Document')
 
 
-class Excerpt(Document):
-    def __init__(self, parent):
-        super(Excerpt, self).__init__(parent=parent)
-
-
-class Update(Document):
-    def __init__(self, parent):
-        super(Update, self).__init__(parent=parent)
-
-    def __unicode__(self):
-        return u'%s (update)' % self.master().__unicode__()
+class Update(object):
+    pass
 
 
 class Tag(object):

management/commands/bake.py

 
             # HTML document
             f = codecs.open(os.path.join(dirpath, 'index.html'), 'w', 'utf-8')
-            f.write(render_to_string('%s.dhtml' % document.type, {'document': document}))
+            f.write(render_to_string(document.type + '.html',
+                                     {'document': document}))
             f.close()
 
             # Markdown document
             components = document.identifier().strip('/').split('/')
-            f = codecs.open(os.path.join(path, *components[:-1] + [components[-1] + '.text']), 'w', 'utf-8')
-            f.write(u'%s\n' % document.body.rstrip())
+            components[-1] += '.text'
+            f = codecs.open(os.path.join(path, *components), 'w', 'utf-8')
+            f.write(u'%s\n' % document.source.rstrip())
             f.close()
 
         count = len(documents)
-        self.stdout.write(
-                'Finished processing %s %s.' %
-                (count, count == 1 and 'document' or 'documents'))
+        self.stdout.write('Finished processing %s %s.\n'
+                          % (count, count == 1 and 'document' or 'documents'))
 
 import mango.settings
 
-if mango.settings.SUBSCRIPTIONS: # since model requires a database, define it only if required
+# since model requires a database, define it only if required
+if mango.settings.SUBSCRIPTIONS:
     class Subscription(models.Model):
         subscriber_name = models.CharField(max_length=100)
         subscriber_email = models.EmailField(max_length=100)
         url = models.URLField(max_length=100)
 
         def __unicode__(self):
-            subscriber = u'%s <%s>' % (self.subscriber_name, self.subscriber_email)
-            return u' '.join((subscriber, '_' * max(1, 48 - len(subscriber)), self.url))
+            subscriber = u'%s <%s>' % (self.subscriber_name,
+                                       self.subscriber_email)
+            return u'%s %s %s' % (subscriber,
+                                  '_' * max(1, 48 - len(subscriber)),
+                                  self.url)
+
+    class SubscriptionMessage(models.Model):
+        """Keeps track of messages sent to subscribers."""
+        url = models.URLField(max_length=100)
+        comment_id = models.CharField(max_length=16)
+        datetime = models.DateTimeField(auto_now=True)
+
+        class Meta:
+            unique_together = ('url', 'comment_id')

settings/__init__.py

+import sys
 import os
 
 from django.utils.encoding import smart_unicode
 except IOError:
     pass
 
-if BASE_URL:
-    BASE_URL = smart_unicode('%s/' % BASE_URL.rstrip('/'))
+if BASE_URL is None:
+    sys.stderr.write('You must set `BASE_URL` in mango/settings/custom.py.\n')
+    sys.exit(1)
+
+BASE_URL = smart_unicode('%s/' % BASE_URL.rstrip('/'))
 
 BODY_WIDTH = 78
 
 DISPLAY_DATE_FORMAT = smart_unicode(DISPLAY_DATE_FORMAT)
 DISPLAY_TIME_FORMAT = smart_unicode(DISPLAY_TIME_FORMAT)
 
-MARKDOWN_DATE_FORMAT = u'%d %B %Y' # e.g. 2 April 2010
-MARKDOWN_TIME_FORMAT = u'%I:%M%p' # e.g. 6:50pm
+MARKDOWN_DATE_FORMAT = u'%d %B %Y'  # e.g. 2 April 2010
+MARKDOWN_TIME_FORMAT = u'%I:%M%p'  # e.g. 6:50pm
 
 DISQUS = bool(DISQUS_API_KEY)
 
 
 UNIX_DOCUMENTS_PATH = _unix_documents_path()
 
-del dirname, os, smart_unicode # tidy the namespace
+del dirname, os, smart_unicode  # tidy the namespace

settings/defaults.py

 DISQUS_SHORTNAME = None
 
 # http://docs.python.org/library/datetime.html#strftime-and-strptime-behavior
-DISPLAY_DATE_FORMAT = '%d %B %Y' # e.g. 2 April 2010
-DISPLAY_TIME_FORMAT = u'%i:%M\u2009%p'.encode('utf-8') # e.g. 6:50|pm (pipe = thin space)
+DISPLAY_DATE_FORMAT = '%d %B %Y'  # e.g. 2 April 2010
+DISPLAY_TIME_FORMAT = u'%i:%M\u2009%p'.encode('utf-8')  # e.g. 6:50|pm (pipe = thin space)
 
 FEED_MAX_POSTS = 20
 
 
 JS = ('static/',)
 
-KILOBYTE_SIZE = 1000
-
 # http://www.freewisdom.org/projects/python-markdown/Available_Extensions
 MARKDOWN_EXTENSIONS = ('def_list', 'fenced_code')
 
 STATIC_FILES = {}
 
 SUBSCRIPTIONS = False
-
-TIME_ZONES = {}
 from django.utils.http import urlencode
 
 import mango.settings
-from mango.decorators import baseurl
 from mango.main import Index
 from mango.utils import logger
 
         try:
             urlopen('%s?%s' % (ping_url,
                     urlencode({'sitemap': '%s%s' % (
-                    baseurl(), sitemap_url[1:])})))
+                    mango.settings.BASE_URL, sitemap_url[1:])})))
         except Exception, error:
             logger.error(error)
 
-import re
+from mango.template.embed import register, urlmatch
 
 
-class CodeSite(object):
-    def __init__(self):
-        pass
+@urlmatch(r'^(?P<protocol>https?)://gist.github.com/(?P<id>\d+)/?$')
+def github(match, **kwargs):
+    return (u'<script src="%s://gist.github.com/%s.js"></script>\n'
+            % (match.group('protocol'), match.group('id')))
 
-
-_match_gist = re.compile(r'^(?P<protocol>https?)://gist.github.com/(?P<id>\d+)/?$').match
-
-class GitHub(CodeSite):
-    def render(self, url, **kwargs):
-        match = _match_gist(url)
-        if match:
-            return u'<script src="%s://gist.github.com/%s.js"></script>' % (
-                    match.group('protocol'), match.group('id'))
-
-
-supported_code_sites = (GitHub(),)
+register(github)

template/embed.py

+import re
+
+
+sites = []
+
+def register(*args):
+    sites.extend(args)
+
+def urlmatch(pattern):
+    matcher = re.compile(pattern, re.VERBOSE).match
+    def outer(fn):
+        def inner(url, **kwargs):
+            match = matcher(url)
+            if match:
+                return fn(match, **kwargs)
+        return inner
+    return outer
+
+def embed():
+    from mango.template import code, video
+    sub = re.compile(r'''
+          ^ <(?P<tag>dt|p)>                     # opening `dt` or `p` tag
+            <a[ ]href="(?P<url>[^"]+)"          # `a` tag with `href` attribute
+            (?:[ ]title="(?P<title>[^"]*)")?    # optional `title` attribute
+            >(?P<text>[^<]+)</a>                # link text
+            </\1>                               # matching closing tag
+          $ ''', re.MULTILINE | re.VERBOSE).sub
+    def markup(match):
+        for site in sites:
+            html = site(**match.groupdict())
+            if html:
+                return html
+        return match.group(0)
+    return lambda text: sub(markup, text)
+embed = embed()

template/video.py

 import re
 import urlparse
 try:
-    from urlparse import parse_qsl # Python >= 2.6
+    from urlparse import parse_qsl  # Python >= 2.6
 except ImportError:
-    from cgi import parse_qsl # Python 2.5
+    from cgi import parse_qsl  # Python 2.5
 
 from django.template import Context, loader
 
-def get_template(template_name):
-    return loader.get_template('video/%s.dhtml' % template_name)
+from mango.template.embed import register, urlmatch
 
 
-class VideoSite(object):
-    def __init__(self, template_name, regex):
-        self.template = get_template(template_name)
-        self.regex = re.compile(regex)
+def videotemplate(fn):
+    template = loader.get_template('video/%s.html' % fn.__name__)
+    return lambda *args, **kwargs: fn(template, *args, **kwargs)
 
-    def render(self, url, **kwargs):
-        match = re.match(self.regex, url)
-        if match:
-            return self.template.render(Context(dict(match.groupdict(), **kwargs)))
+@urlmatch(r'^http://(?:player[.]vimeo[.]com/video|(?:www[.])?vimeo[.]com)/'
+          r'(?P<id>\d+)/?$')
+@videotemplate
+def vimeo(template, match, **context):
+    context['url'] = 'http://player.vimeo.com/video/%s' % match.group('id')
+    return template.render(Context(context))
 
+@videotemplate
+def youtube(template, url, **context):
+    o = urlparse.urlparse(url)
+    if (re.match(r'^https?$', o.scheme) and
+        re.match(r'^(www[.])?youtube[.]com$', o.netloc)):
+        query = dict(parse_qsl(o.query))
+        if 'v' in query:
+            v = query.pop('v')
+            context.update({
+                'query': query,
+                'url': '%s://%s/embed/%s' % (o.scheme, o.netloc, v),
+            })
+            return template.render(Context(context))
 
-_match_vimeo = re.compile(r'^http://(?:player[.]vimeo[.]com/video|(?:www[.])?vimeo[.]com)/(?P<id>\d+)/?$').match
-
-class Vimeo(VideoSite):
-    template = get_template('vimeo')
-
-    def __init__(self):
-        pass
-
-    def render(self, url, **kwargs):
-        match = _match_vimeo(url)
-        if match:
-            context = kwargs
-            context['url'] = u'http://player.vimeo.com/video/%s' % match.group('id')
-            return self.template.render(Context(context))
-
-
-class YouTube(VideoSite):
-    template = get_template('youtube')
-
-    def __init__(self):
-        pass
-
-    def render(self, url, **kwargs):
-        o = urlparse.urlparse(url)
-        if re.match(r'https?$', o.scheme) and re.match(r'(www[.])?youtube[.]com$', o.netloc):
-            query = dict(parse_qsl(o.query))
-            if 'v' in query:
-                context = kwargs
-                context['url'] = u'%s://%s/embed/%s' % (o.scheme, o.netloc, query.pop('v'))
-                context['query'] = query
-                return self.template.render(Context(context))
-
-
-supported_video_sites = (Vimeo(), YouTube())
+register(vimeo, youtube)

templates/404.dhtml

-{% extends "base.dhtml" %}
-{% block title %}404: Page not found{% endblock %}
-{% block content %}
-                <h1>404: Page not found</h1>
-                <p>Oops! The requested page appears not to exist.</p>
-{% endblock %}

templates/404.html

+{% extends "base.html" %}
+
+{% block title %}404: Page not found{% endblock %}
+
+{% block content %}
+        <h1>404: Page not found</h1>
+        <p>Oops! The requested page appears not to exist.</p>
+{% endblock %}

templates/500.html

-{% extends "base.dhtml" %}
+{% extends "base.html" %}
+
 {% block title %}500: Server error{% endblock %}
+
 {% block content %}
-                <h1>500: Server error</h1>
-                <p>Sorry! Something went wrong on the server; the site admins have been alerted to the problem.</p>
+        <h1>500: Server error</h1>
+        <p>Sorry! Something went wrong on the server; the site admins have been alerted to the problem.</p>
 {% endblock %}

templates/_base.dhtml

-<!DOCTYPE html>
-{% load mango %}
-<html>
-  <head>
-{% block head %}
-    <meta charset="utf-8" />
-    <title>{% block title %}{{ SITE_TITLE }}{% endblock %}</title>
-
-  {% block feeds %}
-    <link rel="alternate" type="application/atom+xml" href="{% url mango.feeds.atom %}" />
-  {% endblock %}
-
-  {% for stylesheet in stylesheets %}
-    {{ stylesheet }}
-  {% endfor %}
-
-    <!--[if lt IE 9]>
-    <script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
-    <![endif]-->
-{% endblock %}
-  </head>
-
-  <body>
-{% block body %}
-    <div id="wrap">
-      <header id="header">
-  {% url mango.views.index as path %}
-  {% if request.path == path %}
-        <h1 id="title">{{ SITE_TITLE }}</h1>
-  {% else %}
-        <a id="title" href="{{ path }}">{{ SITE_TITLE }}</a>
-  {% endif %}
-
-  {% block navigation %}
-        <nav>
-          <ul>
-    {% block nav %}
-      {% if ARCHIVES %}{% url mango.views.archives as path %}
-            <li>{% if request.path == path %}Archives{% else %}<a href="{{ path }}">Archives</a>{% endif %}</li>
-      {% endif %}
-      {% if TAGS_PAGE %}{% url mango.views.tags as path %}
-            <li>{% if request.path == path %}Tags{% else %}<a href="{{ path }}">Tags</a>{% endif %}</li>
-      {% endif %}
-      {% if CONTACT_FORM %}{% url mango.views.contact as path %}
-            <li>{% if request.path == path %}Contact{% else %}<a href="{{ path }}">Contact</a>{% endif %}</li>
-      {% endif %}
-    {% endblock %}
-          </ul>
-        </nav>
-  {% endblock %}
-
-        <form action="{% url mango.views.search %}">
-          <div>
-            <label for="query">Search</label>
-            <input type="search" id="query" name="query"{% if terms %} value="{{ terms|to_query_string }}"{% endif %} />
-          </div>
-          <div>
-            <input type="submit" value="Search" />
-          </div>
-        </form>
-      </header>
-
-      <div id="main">
-  {% block content %}
-  {% endblock %}
-      </div>
-    </div>
-
-    <footer id="footer">
-  {% block footer %}
-      <p>Powered by <a href="http://mango.io/">Mango</a></p>
-  {% endblock %}
-    </footer>
-
-  {% block scripts %}
-    {% for script in scripts %}
-    {{ script }}
-    {% endfor %}
-    <script>
-      {% include "js/pilcrows.js" %}
-    </script>
-    {% if request|internal %}
-    <script>
-      {% include "js/flushcache.js" %}
-    </script>
-    {% endif %}
-  {% endblock %}
-
-  {% block analytics %}
-    {% if GOOGLE_ANALYTICS_ID and not settings.DEBUG %}
-    <script>
-      var _gaq = _gaq || [];
-      _gaq.push(['_setAccount', '{{ GOOGLE_ANALYTICS_ID }}']);
-      _gaq.push(['_trackPageview']);
-
-      (function () {
-        var ga = document.createElement('script');
-        ga.type = 'text/javascript';
-        ga.async = true;
-        ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
-        (document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(ga);
-      }());
-    </script>
-    {% endif %}
-  {% endblock %}
-    <!-- Generated by Mango -->
-{% endblock %}
-  </body>
-</html>

templates/_base.html

+<!DOCTYPE html>
+{% load mango %}
+<html>
+  <head>
+{% block head %}
+    <meta charset="utf-8" />
+    <title>{% block title %}{{ SITE_TITLE }}{% endblock %}</title>
+
+  {% block feeds %}
+    <link rel="alternate" type="application/atom+xml" href="{% url mango.feeds.atom %}" />
+  {% endblock %}
+
+  {% for stylesheet in stylesheets %}
+    {{ stylesheet }}
+  {% endfor %}
+
+    <!--[if lt IE 9]>
+    <script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
+    <![endif]-->
+{% endblock %}
+  </head>
+
+{% block bodytag %}
+  <body>
+{% endblock %}
+{% block body %}
+    <div id="wrap">
+      <header id="header">
+  {% url mango.views.index as path %}
+  {% if request.path == path %}
+        <h1 id="title">{{ SITE_TITLE }}</h1>
+  {% else %}
+        <a id="title" href="{{ path }}">{{ SITE_TITLE }}</a>
+  {% endif %}
+
+  {% block navigation %}
+        <nav>
+          <ul>
+    {% block nav %}
+      {% if ARCHIVES %}{% url mango.views.archives as path %}
+            <li>{% if request.path == path %}Archives{% else %}<a href="{{ path }}">Archives</a>{% endif %}</li>
+      {% endif %}
+      {% if TAGS_PAGE %}{% url mango.views.tags as path %}
+            <li>{% if request.path == path %}Tags{% else %}<a href="{{ path }}">Tags</a>{% endif %}</li>
+      {% endif %}
+      {% if CONTACT_FORM %}{% url mango.views.contact as path %}
+            <li>{% if request.path == path %}Contact{% else %}<a href="{{ path }}">Contact</a>{% endif %}</li>
+      {% endif %}
+    {% endblock %}
+          </ul>
+        </nav>
+  {% endblock %}
+
+        <form action="{% url mango.views.search %}">
+          <div>
+            <label for="query">Search</label>
+            <input type="search" id="query" name="query"{% if terms %} value="{{ terms|to_query_string }}"{% endif %} />
+          </div>
+          <div>
+            <input type="submit" value="Search" />
+          </div>
+        </form>
+      </header>
+
+      <div id="main">
+  {% block content %}
+  {% endblock %}
+      </div>
+    </div>
+
+    <footer id="footer">
+  {% block footer %}
+      <p>Powered by <a href="http://mango.io/">Mango</a></p>
+  {% endblock %}
+    </footer>
+
+  {% block scripts %}
+    {% for script in scripts %}
+    {{ script }}
+    {% endfor %}
+    <script>
+      {% include "js/pilcrows.js" %}
+    </script>
+    {% if request|internal %}
+    <script>
+      {% include "js/flushcache.js" %}
+    </script>
+    {% endif %}
+  {% endblock %}
+
+  {% block analytics %}
+    {% if GOOGLE_ANALYTICS_ID and not settings.DEBUG %}
+    <script>
+      var _gaq = _gaq || [];
+      _gaq.push(['_setAccount', '{{ GOOGLE_ANALYTICS_ID }}']);
+      _gaq.push(['_trackPageview']);
+
+      (function () {
+        var ga = document.createElement('script');
+        ga.type = 'text/javascript';
+        ga.async = true;
+        ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
+        (document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(ga);
+      }());
+    </script>
+    {% endif %}
+  {% endblock %}
+    <!-- Generated by Mango -->
+{% endblock %}
+  </body>
+</html>

templates/_contact.dhtml

-{% extends "base.dhtml" %}
-{% load mango %}
-{% block title %}Contact{% endblock %}
-{% block content %}
-            {% block contact_heading %}<h1>Contact</h1>{% endblock %}{% if form %}
-            <form id="contact" method="post">{% csrf_token %}
-                <fieldset>
-                    {% field form.sender_name %}
-                    {% field form.sender_email %}
-                    {% field form.subject %}
-                </fieldset>
-                {% field form.message %}
-                {% field form.cc_sender %}
-                <div><input type="submit" value="Send message" /></div>
-            </form>
-            <script>
-                (function () {
-                    var element, i = arguments.length;
-
-                    function focus() {
-                        if (this.className == 'placeholder') {
-                            this.className = '';
-                            this.value = '';
-                        }
-                    }
-
-                    function blur() {
-                        if (this.value === '') {
-                            this.value = this.getAttribute('placeholder');
-                            this.className = 'placeholder';
-                        }
-                    }
-
-                    while (i--) {
-                        element = document.getElementById(arguments[i]);
-
-                        // check for native support
-                        if ('placeholder' in element) return;
-
-                        // add event listeners (W3C/IE)
-                        if (element.addEventListener) {
-                            element.addEventListener('focus', focus, false);
-                            element.addEventListener('blur', blur, false);
-                        } else if (element.attachEvent) {
-                            element.attachEvent('onfocus', focus);
-                            element.attachEvent('onblur', blur);
-                        }
-
-                        // initialize
-                        blur.apply(element);
-                    }
-                }('sender_name', 'sender_email', 'subject'));
-            </script>{% else %}
-            <p>Thanks for your message. I'll do my best to respond in the not too distant future.</p>{% endif %}
-{% endblock %}

templates/_contact.html

+{% extends "base.html" %}
+
+{% load mango %}
+
+{% block title %}Contact{% endblock %}
+
+{% block content %}
+      {% block contact_heading %}<h1>Contact</h1>{% endblock %}
+    {% if form %}
+      <form id="contact" method="post">{% csrf_token %}
+        <fieldset>
+          {% field form.sender_name %}
+          {% field form.sender_email %}
+          {% field form.subject %}
+        </fieldset>
+        {% field form.message %}
+        {% field form.cc_sender %}
+        <div><input type="submit" value="Send message" /></div>
+      </form>
+    {% else %}
+      <p>Thanks for your message. I'll do my best to respond in the not too distant future.</p>
+    {% endif %}
+{% endblock %}

templates/_document.dhtml

-{% extends "base.dhtml" %}
-{% load mango %}
-{% block title %}{{ document.title_text }}{% endblock %}
-{% block head %}
-    {{ block.super }}{% for stylesheet in document.stylesheets %}
-    {{ stylesheet }}{% endfor %}{% endblock %}
-{% block content %}
-                <article>
-                    <header>
-                        <h1>{{ document.title }}</h1>{% if document.datetime %}
-                        <time datetime="{{ document.datetime|isoformat }}" pubdate="pubdate">{{ document|pubdate }}</time>{% endif %}
-                    </header>
-                    {{ document.html }}
-{% block metadata %}{% if document.author or document.tags or document.has_shorturl %}
-                    <footer class="metadata">{% if document.author or document.has_shorturl %}
-                        <ul>{% if document.author %}{% if document.author.url %}
-                            <li class="author"><a href="{{ document.author.url }}">{{ document.author.name }}</a></li>{% else %}
-                            <li class="author">{{ document.author.name }}</li>{% endif %}{% endif %}{% if document.has_shorturl %}
-                            <li class="shorturl"><a href="{{ document.shorturl }}">Short URL</a></li>{% endif %}
-                        </ul>{% endif %}{% if document.tags %}
-                        <h4>This {{ document.type }} has the following tags:</h4>
-                        <ol>{% for tag in document.tags %}
-                            <li><a href="{% url mango.views.tagged_as tag.slug %}">{{ tag }}</a></li>{% endfor %}
-                        </ol>{% endif %}
-                    </footer>{% endif %}{% endblock %}
-{% block comments %}{% if comments or new_comment %}
-                    {% block comments_heading %}<h2 id="comments">Comments</h2>{% endblock %}{% for comment in comments %}{% include "comment.dhtml" %}{% endfor %}{% if new_comment %}{{ new_comment }}{% if COMMENTS_REQUIRE_APPROVAL %}
-                    <p><strong>Your comment is awaiting moderation.</strong></p>{% endif %}{% endif %}{% endif %}{% if DISQUS and thread.allow_comments %}
-                    {% block respond_heading %}<h3 id="respond">Respond</h3>{% endblock %}
-                    {{ form.non_field_errors }}
-                    <form id="comment" method="post">{% csrf_token %}
-                        <fieldset>
-                            {% field form.author_name %}
-                            {% field form.author_email %}
-                            {% field form.author_url %}
-                        </fieldset>
-                        {% field form.message %}{% if SUBSCRIPTIONS %}
-                        {% field form.subscribe %}{% endif %}
-                        <div><input type="submit" value="Submit comment" /></div>
-                    </form>
-                    <script>
-                        (function () {
-                            var element, i = arguments.length;
-
-                            function focus() {
-                                if (this.className == 'placeholder') {
-                                    this.className = '';
-                                    this.value = '';
-                                }
-                            }
-
-                            function blur() {
-                                if (this.value === '') {
-                                    this.value = this.getAttribute('placeholder');
-                                    this.className = 'placeholder';
-                                }
-                            }
-
-                            while (i--) {
-                                element = document.getElementById(arguments[i]);
-
-                                // check for native support
-                                if ('placeholder' in element) return;
-
-                                // add event listeners (W3C/IE)
-                                if (element.addEventListener) {
-                                    element.addEventListener('focus', focus, false);
-                                    element.addEventListener('blur', blur, false);
-                                } else if (element.attachEvent) {
-                                    element.attachEvent('onfocus', focus);
-                                    element.attachEvent('onblur', blur);
-                                }
-
-                                // initialize
-                                blur.apply(element);
-                            }
-                        }('author_name', 'author_email', 'author_url'));
-                    </script>{% endif %}
-{% endblock %}
-                </article>
-{% endblock %}
-{% block scripts %}
-    {{ block.super }}{% for script in document.scripts %}
-    {{ script }}{% endfor %}{% endblock %}

templates/_document.html

+{% extends "base.html" %}
+
+{% load mango %}
+
+{% block title %}{{ document.title_text }}{% endblock %}
+
+{% block head %}
+    {{ block.super }}
+  {% for stylesheet in document.stylesheets %}
+    {{ stylesheet }}
+  {% endfor %}
+{% endblock %}
+
+{% block content %}
+        <article{% if document.meta.id %} id="{{ document.meta.id }}"{% endif %}>
+          <header>
+            <h1>{{ document.title }}</h1>
+          {% if document.datetime %}
+            <time datetime="{{ document.datetime|isoformat }}" pubdate="pubdate">{{ document|pubdate }}</time>
+          {% endif %}
+          </header>
+          {{ document.html }}
+  {% block metadata %}
+        {% if document.author or document.tags or document.has_shorturl %}
+          <footer class="metadata">
+          {% if document.author or document.has_shorturl %}
+            <ul>
+            {% if document.author %}
+              <li class="author">
+              {% if document.author.url %}
+                <a href="{{ document.author.url }}">{{ document.author.name }}</a>
+              {% else %}
+                {{ document.author.name }}
+              {% endif %}
+              </li>
+            {% endif %}
+            {% if document.has_shorturl %}
+              <li class="shorturl"><a href="{{ document.shorturl }}">Short URL</a></li>
+            {% endif %}
+            </ul>
+          {% endif %}
+          {% if document.tags %}
+            <h4>This {{ document.type }} has the following tags:</h4>
+            <ol>
+            {% for tag in document.tags %}
+              <li><a href="{% url mango.views.tagged_as tag.slug %}">{{ tag }}</a></li>
+            {% endfor %}
+            </ol>
+          {% endif %}
+          </footer>
+        {% endif %}
+  {% endblock %}
+  {% block comments %}
+      {% if comments or new_comment %}
+          {% block comments_heading %}<h2 id="comments">Comments</h2>{% endblock %}
+          {% for comment in comments %}{% include "comment.html" %}{% endfor %}
+        {% if new_comment %}
+          {{ new_comment }}
+          {% if COMMENTS_REQUIRE_APPROVAL %}
+          <p><strong>Your comment is awaiting moderation.</strong></p>
+          {% endif %}
+        {% endif %}
+      {% endif %}
+      {% if DISQUS and thread.allow_comments %}
+          {% block respond_heading %}<h3 id="respond">Respond</h3>{% endblock %}
+          {{ form.non_field_errors }}
+          <form id="comment" method="post">{% csrf_token %}
+            <fieldset>
+              {% field form.author_name %}
+              {% field form.author_email %}
+              {% field form.author_url %}
+            </fieldset>
+            {% field form.message %}
+          {% if SUBSCRIPTIONS %}
+            {% field form.subscribe %}
+          {% endif %}
+            <div><input type="submit" value="Submit comment" /></div>
+          </form>
+      {% endif %}
+  {% endblock %}
+        </article>
+{% endblock %}
+
+{% block scripts %}
+    {{ block.super }}
+  {% for script in document.scripts %}
+    {{ script }}
+  {% endfor %}
+{% endblock %}

templates/archives.dhtml

-{% extends "base.dhtml" %}
-{% load mango %}
-{% block title %}Archives{% endblock %}
-{% block content %}
-            <h1>Archives</h1>{% if archives %}
-            <ol id="archives">{% for year, month, posts in archives %}
-                <li>
-                    <h2>{{ month|month }} {{ year }}</h2>
-                    <ol>{% for post in posts %}
-                        <li>
-                            <a href="{{ post.permalink }}">{{ post.title }}</a>
-                            <time datetime="{{ post.datetime|isoformat }}">{{ post|pubdate }}</time>
-                        </li>{% endfor %}
-                    </ol>
-                </li>{% endfor %}
-            </ol>{% else %}
-            <p>The archives are currently empty.</p>{% endif %}
-{% endblock %}

templates/archives.html

+{% extends "base.html" %}
+
+{% load mango %}
+
+{% block title %}Archives{% endblock %}
+
+{% block content %}
+        <h1>Archives</h1>
+      {% if archives %}
+        <ol id="archives">
+        {% for year, month, posts in archives %}
+          <li>
+            <h2>{{ month|month }} {{ year }}</h2>
+            <ol>
+            {% for post in posts %}
+              <li>
+                <a href="{{ post.permalink }}">{{ post.title }}</a>
+                <time datetime="{{ post.datetime|isoformat }}">{{ post|pubdate }}</time>
+              </li>
+            {% endfor %}
+            </ol>
+          </li>
+        {% endfor %}
+        </ol>
+      {% else %}
+        <p>The archives are currently empty.</p>
+      {% endif %}
+{% endblock %}

templates/base.dhtml

-{% extends "_base.dhtml" %}
-{% block scripts %}
-{{ block.super }}
-    <script>
-        (function (undefined) {
-            var top, wrap = document.getElementById('wrap'), style = wrap.style;
-
-            if (style.webkitBorderImage === undefined &&
-                style.MozBorderImage === undefined &&
-                style.borderImage === undefined) {
-
-                if (wrap.className) {
-                    wrap.className += ' noborderimage';
-                } else {
-                    wrap.className = 'noborderimage';
-                }
-                top = document.createElement('div');
-                top.id = 'topfix';
-                wrap.appendChild(top);
-            }
-        }());
-    </script>{% endblock %}

templates/base.html

+{% extends "_base.html" %}
+
+{% block scripts %}
+    {{ block.super }}
+    <script>
+      (function (undefined) {
+        var top, wrap = document.getElementById('wrap'), style = wrap.style;
+
+        if (style.webkitBorderImage === undefined &&
+            style.MozBorderImage === undefined &&
+            style.borderImage === undefined) {
+
+          if (wrap.className) {
+            wrap.className += ' noborderimage';
+          } else {
+            wrap.className = 'noborderimage';
+          }
+          top = document.createElement('div');
+          top.id = 'topfix';
+          wrap.appendChild(top);
+        }
+      }());
+    </script>
+{% endblock %}

templates/category.dhtml

-{% extends "base.dhtml" %}
-{% load mango %}
-{% block title %}{{ category.name|smart_capfirst }}{% endblock %}
-{% block stylesheets %}{{ block.super }}{% for stylesheet in category.pages|combine:category.posts|required:"stylesheets" %}
-    {{ stylesheet }}{% endfor %}{% endblock %}
-{% block content %}
-            <h1>{{ category.name|smart_capfirst }}</h1>{% if category.html %}
-            {{ category.html }}{% endif %}
-            {% if category.pages %}
-            <h2>Pages</h2>{% for document in category.pages %}{% include "excerpt.dhtml" %}{% endfor %}{% endif %}
-            {% if category.posts %}
-            <h2>Posts</h2>{% for document in category.posts %}{% include "excerpt.dhtml" %}{% endfor %}{% endif %}
-            {% if category.subcategories %}
-            <h2>Subcategor{{ category.subcategories|length|pluralize:"y,ies" }}</h2>
-            <ol id="subcategories">{% for subcategory in category.subcategories %}
-                <li><a href="{{ subcategory.urls.canon.rel }}">{{ subcategory.name|smart_capfirst }}</a></li>{% endfor %}
-            </ol>{% endif %}{% if not category.pages and not category.posts and not category.subcategories %}
-            <p>This category is currently empty.</p>{% endif %}
-{% endblock %}
-{% block scripts %}{{ block.super }}{% for script in category.pages|combine:category.posts|required:"scripts" %}
-    {{ script }}{% endfor %}{% endblock %}

templates/category.html

+{% extends "base.html" %}
+
+{% load mango %}
+
+{% block title %}{{ category.name|smart_capfirst }}{% endblock %}
+
+{% block stylesheets %}
+    {{ block.super }}
+  {% for stylesheet in category.pages|combine:category.posts|required:"stylesheets" %}
+    {{ stylesheet }}
+  {% endfor %}
+{% endblock %}
+
+{% block content %}
+        <h1>{{ category.name|smart_capfirst }}</h1>
+      {% if category.html %}
+        {{ category.html }}
+      {% endif %}
+      {% if category.pages %}
+        <h2>Pages</h2>