Source

django-in-a-box / docs / pavement.py

# -*- coding: utf-8 -*-

import codecs
import mimetypes
import os
import re
import wsgiref.simple_server

import jinja2
import markdown
try:
    import webob
except ImportError:
    webob = None

from paver.easy import *


docroot = path(__file__).abspath().dirname()

options(
    docroot = docroot,
    template_dir = docroot / '_templates',
    static_dir = docroot / '_static',
    html_dir = docroot / '_html',
    temp_dir = docroot / '_tmp',
)

options(
    markdown_file_extensions = ('.md', '.mdown', '.markdown'),
    markdown_extensions = (
        'abbr',
        'codehilite',
        'def_list',
        'footnotes',
        'headerid',
        'tables',
        'toc'),
    
    jinja2_env = jinja2.Environment(
        loader=jinja2.FileSystemLoader([options.template_dir])),
)


@task
def sync_static(options):
    """Synchronize static files from docroot/_static -> docroot/_html."""
    
    options.html_dir.makedirs()
    
    sh('rsync -vax --ignore-errors --exclude=".*" --exclude="_*" '
       '_static/ _html/', cwd=options.docroot)


@task
def sync_html(options):
    """Synchronize built HTML and static files into docroot/_html."""
    
    options.html_dir.makedirs()
    sh('rsync -vax --delete --ignore-errors '
       '--exclude=".*" --exclude="_*" _tmp/ _static/ _html/', cwd=options.docroot)


@task
def clean_temp(options):
    """Clean old build HTML from docroot/_tmp."""
    
    if options.temp_dir.exists():
        options.temp_dir.rmtree()
    options.temp_dir.makedirs()


@task
def clean_html(options):
    """Clean built HTML from docroot/_html."""
    
    if options.html_dir.exists():
        options.html_dir.rmtree()
    options.html_dir.makedirs()


@task
@needs('clean_temp')
def build(options):
    """Generate and output HTML into the `docroot/_html` directory."""
    
    doc_template = options.jinja2_env.get_template('document.html')
    
    for filename in options.docroot.walkfiles():
        rel_filename = options.docroot.relpathto(filename)
        
        if rel_filename.startswith('_'):
            continue # Ignore things like _static, _html, _templates, etc.
        if filename.ext not in options.markdown_file_extensions:
            continue
        
        ## Content
        doc_text = read_from(filename)
        doc_html = render(doc_text)
        
        ## Title
        if rel_filename == 'index.md':
            title = 'Index'
        else:
            title = get_title(doc_html)
        
        if title is None:
            title = re.sub(r'[-_]+', ' ', rel_filename.basename()).title()
        
        ## Breadcrumbs
        crumbs = rel_filename.splitall()
        if crumbs and crumbs[0] == '':
            del crumbs[0]
        if crumbs and crumbs[-1] == 'index.md':
            del crumbs[-1]
        if crumbs and path(crumbs[-1]).ext == '.md':
            crumbs[-1] = path(crumbs[-1]).splitext()[0]
        
        full_html = doc_template.render({
            'title': title,
            'content': doc_html,
            'crumbs': crumbs})
        output_filename = options.temp_dir / rel_filename.splitext()[0] + '.html'
        write_to(output_filename, data=full_html, relto=options.temp_dir)
    
    call_task('sync_html')


@task
@cmdopts([
    ('build', 'b', 'Build the documentation before serving.'),
    ('port=', 'p', 'Specify which port to serve on (default 8008).'),
    ('interface=', 'i', 'Serve on a given interface (default 127.0.0.1).')])
def serve(options):
    """Serve the documentation via HTTP."""
    
    if options.get('build', False):
        call_task('build')
    
    server = DocServer(options)
    interface = options.get('interface', '127.0.0.1')
    port = int(options.get('port', 8008))
    print 'Serving documentation on http://127.0.0.1:8008/'
    server.serve_forever(interface=interface, port=port)


def read_from(filename, encoding='utf-8'):
    """Read data from a filename, using an encoding."""
    
    if encoding is None:
        fp = open(filename)
    else:
        fp = codecs.open(filename, encoding=encoding)
    
    try:
        return fp.read()
    finally:
        fp.close()


def write_to(filename, data=None, encoding='utf-8', relto=None):
    """Write the given data to a filename, using an encoding."""
    
    # Ensure the path to the file exists.
    path(filename).dirname().makedirs()
    
    def write():
        if encoding is None:
            fp = open(filename, 'w')
        else:
            fp = codecs.open(filename, encoding=encoding, mode='w')
    
        try:
            if data is not None:
                fp.write(data)
        finally:
            fp.close()
    
    if relto is not None:
        dry_path = path(relto).relpathto(filename)
    else:
        dry_path = filename
    
    dry('Writing data to "%s"' % dry_path, write)


def render(text):
    """Render a given piece of text using Markdown."""
    
    return markdown.markdown(text, extensions=options.markdown_extensions)


def get_title(html):
    """Extract a meaningful title from a HTML document."""
    
    match = re.search(r'<!-- ?title:(.+)-->', html)
    if match:
        return match.group(1).strip()
    
    match = re.search(r'<h1[^>]*>([^<]+)</h1>', html)
    if match:
        return match.group(1)
    return None


class DocServer(object):
    
    """Simple HTTP application for serving rendered documentation."""
    
    def __init__(self, options):
        if webob is None:
            raise ImportError('WebOb not found. Try `easy_install webob`.')
        
        self.options = options
        self.htroot = options.html_dir
    
    def __call__(self, environ, start_response):
        """Call the WSGI application object."""
        
        request = webob.Request(environ)
        response = self.get_response(request)
        return response(environ, start_response)
    
    def get_response(self, request):
        path = request.path_info.lstrip('/') or 'index.html'
        
        if (self.htroot / path).isfile():
            return self.serve_file(self.htroot / path)
        
        elif (self.htroot / (path + '.html')).isfile():
            return self.serve_file(self.htroot / (path + '.html'))
        
        elif (self.htroot / path / 'index.html').isfile():
            if path.endswith('/'):
                return self.serve_file(self.htroot / path / 'index.html')
            else:
                return self.temp_redirect(path + '/')
        
        elif (self.htroot / path).isdir():
            if path.endswith('/'):
                return self.list_directory(self.htroot / path)
            else:
                return self.temp_redirect(path + '/')
        
        else:
            return self.not_found(path)
    
    def temp_redirect(self, location):
        return webob.Response(status=302, location=location)
    
    def serve_file(self, filepath, content_type=None):
        response = webob.Response()
                
        if filepath.endswith('.html') and content_type is None:
            response.content_type = 'application/xhtml+xml'
        elif content_type is None:
            response.content_type = mimetypes.guess_type(filepath)[0]
        else:
            response.content_type = content_type
        
        def reader(chunk_size=4096):
            fileobj = open(filepath, 'rb')
            try:
                data = fileobj.read(chunk_size)
                while data:
                    yield data
                    data = fileobj.read(chunk_size)
            finally:
                fileobj.close()
        
        response.app_iter = reader()
        response.content_length = os.path.getsize(filepath)
        
        return response
    
    def not_found(self, request_path):
        response = webob.Response(status=404)
        
        html_exts = ('.html', '.htm', '.xhtml', '')
        if path(request_path).splitext()[1] not in html_exts:
            # Don't return HTML.
            del response.content_type
            del response.content_length
            return response
        
        content_template = self.options.jinja2_env.get_template('404.html')
        document_template = self.options.jinja2_env.get_template('document.html')
        
        content = content_template.render({'path': request_path})
        title = 'Not Found'
        
        response.content_type = 'application/xhtml+xml'
        response.unicode_body = document_template.render({
            'content': content,
            'title': title})
        
        return response
    
    def list_directory(self, directory):
        response = webob.Response()
        
        directory = path(directory)
        rel_directory = self.htroot.relpathto(directory) / ''
        sub_dirs, files = [], []
        for filename in directory.listdir():
            rel_filename = directory.relpathto(filename)
            if re.match(r'^[_\.]', rel_filename):
                continue
            
            if filename.isdir():
                # Directories are displayed with a trailing slash.
                sub_dirs.append(rel_filename / '')
            elif filename.isfile():
                files.append(rel_filename)
        
        content_template = self.options.jinja2_env.get_template('listing.html')
        document_template = self.options.jinja2_env.get_template('document.html')
        
        title = 'Directory listing for ' + rel_directory
        content = content_template.render({
            'directory': rel_directory,
            'sub_directories': sorted(sub_dirs),
            'files': sorted(files)})
        
        response.content_type = 'application/xhtml+xml'
        response.unicode_body = document_template.render({
            'content': content,
            'title': title})
        
        return response
    
    def serve_forever(self, interface='127.0.0.1', port=8008):
        httpd = wsgiref.simple_server.make_server(interface, port, self)
        try:
            httpd.serve_forever()
        except KeyboardInterrupt:
            sys.exit(0)
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.