Commits

Eric Knibbe committed 63f506a Draft

adding restview

  • Participants
  • Parent commits 9af00dd

Comments (0)

Files changed (11)

 		in reStructuredText files and lists section titles in the function pop-up
 		menu. Recognizes both standard reST directives and Sphinx additions.</dd>
 			
+	<dt><a href="https://bitbucket.org/EricFromCanada/ericfromcanada.bitbucket.org/raw/default/python/restview/">restview</a></dt>
+	<dd>My branch of the excellent <a href="http://mg.pov.lt/restview/">restview</a>, a
+		tool for previewing reST documents in a web browser, with some improvements
+		I made to the CSS, syntax highlighting, and request handling components.</dd>
+			
 	<dt><a href="https://bitbucket.org/EricFromCanada/pygments-main">Lasso lexer for Pygments</a></dt>
 	<dd>Contributed a lexer for the Lasso programming language for the 
 		<a href="http://pygments.org/">Pygments</a> syntax highlighter. Expect to
 		see it included in version 1.6.</dd>
-	
 			
 </dl>
 
 <a href="https://twitter.com/EricFromCanada">Twitter</a>
+<!-- I'd like to replace this with a reST file so it displays when browsing Bitbucket,
+and use docutils to auto-generate an HTML version before each push -->
 </body>
 </html>

python/restview/MANIFEST.in

+include restview
+include sample.rst
+include test.py
+include src/restview/default.css
+include Makefile

python/restview/Makefile

+PYTHON = python
+
+FILE_WITH_VERSION = src/restview/restviewhttp.py
+FILE_WITH_CHANGELOG = README.txt
+VCS_STATUS = bzr status
+VCS_EXPORT = bzr export
+VCS_TAG = bzr tag
+VCS_COMMIT_AND_PUSH = bzr ci -m "Post-release version bump" && bzr push
+
+
+.PHONY: default
+default: all
+
+
+.PHONY: all
+all:
+	@echo "Nothing to do"
+
+
+.PHONY: check test
+check test:
+	$(PYTHON) test.py
+
+.PHONY: dist
+dist:
+	$(PYTHON) setup.py sdist
+
+.PHONY: distcheck
+distcheck:
+	# Bit of a chicken-and-egg here, but if the tree is unclean, make
+	# distcheck will fail.
+ifndef FORCE
+	@test -z "`$(VCS_STATUS) 2>&1`" || { echo; echo "Your working tree is not clean" 1>&2; $(VCS_STATUS); exit 1; }
+endif
+	make dist
+	pkg_and_version=`$(PYTHON) setup.py --name`-`$(PYTHON) setup.py --version` && \
+	rm -rf tmp && \
+	mkdir tmp && \
+	$(VCS_EXPORT) tmp/tree && \
+	cd tmp && \
+	tar xvzf ../dist/$$pkg_and_version.tar.gz && \
+	diff -ur $$pkg_and_version tree -x PKG-INFO -x setup.cfg -x '*.egg-info' && \
+	cd $$pkg_and_version && \
+	make dist check && \
+	cd .. && \
+	mkdir one two && \
+	cd one && \
+	tar xvzf ../../dist/$$pkg_and_version.tar.gz && \
+	cd ../two/ && \
+	tar xvzf ../$$pkg_and_version/dist/$$pkg_and_version.tar.gz && \
+	cd .. && \
+	diff -ur one two -x SOURCES.txt && \
+	cd .. && \
+	rm -rf tmp && \
+	echo "sdist seems to be ok"
+
+.PHONY: releasechecklist
+releasechecklist:
+	@$(PYTHON) setup.py --version | grep -qv dev || { \
+	    echo "Please remove the 'dev' suffix from the version number in $(FILE_WITH_VERSION)"; exit 1; }
+	@$(PYTHON) setup.py --long-description | rst2html --exit-status=2 > /dev/null
+	@ver_and_date="`$(PYTHON) setup.py --version` (`date +%Y-%m-%d`)" && \
+	    grep -q "^$$ver_and_date$$" $(FILE_WITH_CHANGELOG) || { \
+	        echo "$(FILE_WITH_CHANGELOG) has no entry for $$ver_and_date"; exit 1; }
+	make distcheck
+
+.PHONY: release
+release: releasechecklist
+	# I'm chicken so I won't actually do these things yet
+	@echo "Please run"
+	@echo
+	@echo "  $(PYTHON) setup.py sdist register upload && $(VCS_TAG) `$(PYTHON) setup.py --version`"
+	@echo
+	@echo "Please increment the version number in $(FILE_WITH_VERSION)"
+	@echo "and add a new empty entry at the top of the changelog in $(FILE_WITH_CHANGELOG), then"
+	@echo
+	@echo '  $(VCS_COMMIT_AND_PUSH)'
+	@echo
+

python/restview/README.txt

+========
+restview
+========
+
+A viewer for ReStructuredText documents that renders them on the fly.
+
+Pass the name of a ReStructuredText document to restview, and it will
+launch a web server on localhost:random-port and open a web browser.
+Every time you reload the page, restview will reload the document from
+disk and render it.  This is very convenient for previewing a document
+while you're editing it.
+
+You can also pass the name of a directory, and restview will recursively
+look for files that end in .txt or .rst and present you with a list.
+
+Finally, you can make sure your Python package has valid ReStructuredText
+in the long_description field by using ::
+
+  restview -e 'python setup.py --long-description'
+
+This is so useful restview has a shortcut for it ::
+
+  restview --long-description
+
+
+Changelog
+=========
+
+1.3.0 (unreleased)
+------------------
+
+- Automatically reload the web page when the source file changes (LP#965746).
+  Patch by speq (sp@bsdx.org).
+
+- New option: restview --long-description.
+
+1.2.2 (2010-09-14)
+------------------
+
+- setup.py no longer requires docutils (LP#637423).
+
+1.2.1 (2010-09-12)
+------------------
+
+- Handle spaces and other special characters in URLs (LP#616335).
+
+- Don't linkify filenames inside external references (LP#634827).
+
+1.2 (2010-08-06)
+----------------
+
+- "SEVERE" docutils errors now display a message and unformatted file in
+  the browser, instead of a traceback on the console.
+- New command-line option, -e COMMAND.
+- Added styles for admonitions; many other important styles are still missing.
+
+1.1.3 (2009-10-25)
+------------------
+
+- Spell 'extras_require' correctly in setup.py (LP#459840).
+- Add a MANIFEST.in for complete source distributions (LP#459845).
+
+1.1.2 (2009-10-14)
+------------------
+
+- Fix for 'localhost' name resolution error on Mac OS X.
+
+1.1.1 (2009-07-13)
+------------------
+
+- Launches the web server in the background.
+
+1.1.0 (2008-08-26)
+------------------
+
+- Accepts any number of files and directories on the command line.
+
+1.0.1 (2008-07-26)
+------------------
+
+- New option: --css.  Accepts a filename or a HTTP/HTTPS URL.
+
+1.0.0 (2008-07-26)
+------------------
+
+- Bumped version number to reflect the stability.
+- Minor CSS tweaks.
+
+0.0.5 (2007-09-29)
+------------------
+
+- Create links to other local files referenced by name.
+- Use pygments (if available) to syntax-highlight doctest blocks.
+- Handle JPEG images.
+
+0.0.4 (2007-09-28)
+------------------
+
+- Remove the unstable Gtk+ version.
+
+0.0.3 (2007-09-28)
+------------------
+
+- Use setuptools for packaging.
+
+0.0.2 (2007-01-21)
+------------------
+
+- Browser-based version.
+- Command line options -l, -b (thanks to Charlie Shepherd).
+- CSS tweaks.
+- Unicode bugfix.
+- Can browse directory trees.
+- Can serve images.
+
+0.0.1 (2005-12-06)
+------------------
+
+- PyGtk+ version with GtkMozEmbed.  Not very stable.
+

python/restview/restview

+#!/usr/bin/python
+import sys, os
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
+
+from restview.restviewhttp import main
+main()
+

python/restview/sample.rst

+================================
+Sample ReStructuredText document
+================================
+
+This is a sample ReStructuredText document.
+
+Lists
+-----
+
+Here we have a numbered list
+
+1. Four
+2. Five
+3. Six
+
+and a regular list.
+
+- One
+- Two
+- Three
+

python/restview/setup.py

+#!/usr/bin/env python
+import os
+from setuptools import setup
+
+def read(filename):
+    return open(os.path.join(os.path.dirname(__file__), filename)).read()
+
+def get_version(filename='src/restview/restviewhttp.py'):
+    for line in read(filename).splitlines():
+        if line.startswith('__version__'):
+            d = {}
+            exec line in d
+            return d['__version__']
+    raise AssertionError("couldn't find __version__ in %s" % filename)
+
+version = get_version()
+
+setup(
+    name='restview',
+    version=version,
+    author='Marius Gedminas',
+    author_email='marius@gedmin.as',
+    url='http://mg.pov.lt/restview/',
+    download_url='http://cheeseshop.python.org/pypi/restview',
+    description='ReStructuredText viewer',
+    long_description=read('README.txt'),
+    license='GPL',
+    classifiers=[
+        'Development Status :: 5 - Production/Stable',
+        'Environment :: Web Environment',
+        'Intended Audience :: Developers',
+        'Intended Audience :: End Users/Desktop',
+        'License :: OSI Approved :: GNU General Public License (GPL)',
+        'Programming Language :: Python',
+        'Operating System :: OS Independent',
+        'Topic :: Documentation',
+        'Topic :: Internet :: WWW/HTTP :: HTTP Servers',
+        'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
+        'Topic :: Software Development :: Documentation',
+        'Topic :: Text Processing :: Markup',
+    ],
+
+    packages=['restview'],
+    package_dir={'':'src'},
+    include_package_data=True,
+    install_requires=['docutils'],
+    extras_require={'syntax': ['pygments']},
+    zip_safe=False,
+    entry_points="""
+    [console_scripts]
+    restview = restview.restviewhttp:main
+    """,
+)

python/restview/src/restview/__init__.py

Empty file added.

python/restview/src/restview/default.css

+/*
+ * Stylesheet for ReStructuredText by Marius Gedminas.
+ * (I didn't like the default one)
+ *
+ * XXX: I'm doing it incorrectly:
+ * http://docutils.sourceforge.net/docs/howto/html-stylesheets.html
+ */
+
+body {
+    font-family: Verdana, sans;
+    margin: 3em;
+    max-width: 50em;
+}
+
+p {
+    text-align: justify;
+}
+p.rubric {
+    text-align: center;
+    font-weight: bold;
+}
+
+/* Monospace text */
+
+tt {
+    font-family: Andale Mono, Courier New, monospace;
+    color: #234F32;
+}
+
+/* Definition lists */
+
+dt {
+    font-weight: bold;
+}
+dd {
+    margin-bottom: 1em;
+}
+
+/* Admonitions */
+
+div.admonition p.admonition-title, div.hint p.admonition-title,
+div.important p.admonition-title, div.note p.admonition-title,
+div.tip p.admonition-title {
+    font-weight: bold;
+}
+
+div.attention p.admonition-title, div.caution p.admonition-title,
+div.danger p.admonition-title, div.error p.admonition-title,
+div.warning p.admonition-title {
+    color: red;
+    font-weight: bold;
+}
+
+div.admonition, div.attention, div.caution, div.danger, div.error,
+div.hint, div.important, div.note, div.tip, div.warning {
+    margin: 2em;
+    background: #fff7cc;
+    padding: 1em;
+}
+
+/* Doctest blocks */
+
+pre.doctest-block {
+    color: #008000;
+}
+
+/* Literal blocks */
+
+pre.literal-block {
+    color: #AA4400;
+    margin-left: 40px;
+}
+
+/* Indented quotations */
+
+blockquote > p {
+    color: #000080;
+}
+
+/* Tables */
+
+table {
+    border-top: 2px solid black;
+    border-bottom: 2px solid black;
+    border-left: none;
+    border-right: none;
+    border-collapse: collapse;
+    margin: 0.5em 0;
+}
+tr:first-child > th {
+    border-top: none;
+}
+th {
+    border-top: hidden;
+    border-left: hidden;
+    border-right: hidden;
+    border-bottom: none;
+    text-align: left;
+}
+tr:first-child > td {
+    border-top: 1px solid black;
+}
+table[rules=none] tr:first-child > td {
+    border-top: none;
+}
+td {
+    border-top: hidden;
+    border-left: hidden;
+    border-right: hidden;
+    border-bottom: none;
+    padding: 1px 4px;
+}
+td, th {
+    padding-left: 1em;
+}
+td:first-child,
+th:first-child {
+    padding-left: 4px;
+}
+
+td p:first-child {
+    margin-top: 0;
+}
+th.field-name, th.docinfo-name {
+  white-space: nowrap;
+  padding-left: 0;
+}
+
+/* Table of Contents */
+
+p.topic-title {
+    font-weight: bold;
+    font-size: 120%;
+}
+
+a.toc-backref {
+    color: inherit;
+    text-decoration: none;
+}
+
+/* Footnotes */
+
+a.footnote-reference,
+a.fn-backref {
+    font-size: xx-small;
+    vertical-align: super;
+    line-height: normal;
+    text-decoration: none;
+}
+a.footnote-reference:hover,
+a.fn-backref:hover {
+    text-decoration: underline;
+}
+
+table.footnote {
+    border: none;
+    margin-top: 0.5em;
+    margin-bottom: 0.5em;
+    margin-left: 20px;
+    margin-right: 0.5em;
+    font-size: small;
+}
+table.footnote td {
+    border: none;
+    padding-top: 1em;
+    padding-left: 0px;
+}
+table.footnote tr:first-child > td.label {
+    border-top: 1px solid #eee;
+    padding-left: 20px;
+    padding-right: 20px;
+}
+
+table.footnote + table.footnote {
+    margin-top: 0;
+}
+table.footnote + table.footnote td {
+    border: none;
+    padding-top: 0;
+}
+
+/* System messages (aka errors) */
+
+div.system-message {
+    border-left: 3px double red;
+    margin-left: 19px;
+    padding-left: 19px;
+    padding-top: 10px;
+    padding-bottom: 10px;
+    color: red;
+}
+div.system-messages h1 {
+    color: red;
+}
+div.system-message p.system-message-title {
+    margin-top: 0;
+    font-weight: bold;
+}
+
+/* Sidebars */
+
+div.sidebar {
+    margin: 2em;
+    background: #fff7cc;
+    padding: 1em;
+    width: 40%;
+    float: right;
+    clear: right;
+}
+p.sidebar-title {
+    font-weight: bold;
+    font-size: larger;
+}
+p.sidebar-subtitle {
+    font-weight: bold;
+}
+
+/* Rules taken from the original */
+
+/* used to remove borders from tables and images */
+.borderless, table.borderless td, table.borderless th {
+  border: 0 }
+
+table.borderless td, table.borderless th {
+  /* Override padding for "table.docutils td" with "! important".
+     The right padding separates the table cells. */
+  padding: 0 0.5em 0 0 ! important }
+
+.first {
+  /* Override more specific margin styles with "! important". */
+  margin-top: 0 ! important }
+
+.last, .with-subtitle {
+  margin-bottom: 0 ! important }
+
+.hidden {
+  display: none }
+
+blockquote.epigraph {
+  margin: 2em 5em ; }
+
+div.topic {
+  margin: 2em }
+
+div.abstract {
+  margin: 2em 5em }
+
+div.abstract p.topic-title {
+  font-weight: bold ;
+  text-align: center }
+
+div.dedication {
+  margin: 2em 5em ;
+  text-align: center ;
+  font-style: italic }
+
+div.dedication p.topic-title {
+  font-weight: bold ;
+  font-style: normal }
+
+div.figure {
+  margin-left: 2em ;
+  margin-right: 2em }
+
+div.line-block {
+  display: block ;
+  margin-top: 1em ;
+  margin-bottom: 1em }
+
+div.line-block div.line-block {
+  margin-top: 0 ;
+  margin-bottom: 0 ;
+  margin-left: 1.5em }
+
+ol.simple, ul.simple {
+  margin-bottom: 1em }
+
+span.option {
+  white-space: nowrap }
+
+span.problematic {
+  color: red }
+
+ul.auto-toc {
+  list-style-type: none }
+
+/* ... many more are missing ... */

python/restview/src/restview/restviewhttp.py

+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+"""
+HTTP-based ReStructuredText viewer.
+
+Usage:
+    restviewhttp [options] filename.rst
+or
+    restviewhttp [options] directory
+or
+    restviewhttp [options] -e "command"
+or
+    restviewhttp [options] --long-description
+or
+    restviewhttp --help
+
+Needs docutils and a web browser.  Will syntax-highlight code or doctest blocks
+if you have pygments installed.
+"""
+
+import os
+import re
+import sys
+import socket
+import optparse
+import threading
+import webbrowser
+import BaseHTTPServer
+import SocketServer
+import cgi
+import urllib
+import time
+import urlparse
+
+from docutils import core
+from docutils.writers import html4css1
+
+try:
+    import pygments
+    from pygments import lexers, formatters
+except ImportError:
+    pygments = None
+
+__version__ = "1.3.0dev"
+
+
+class MyRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
+    """HTTP request handler that renders ReStructuredText on the fly."""
+
+    server_version = "restviewhttp/" + __version__
+    last_atime = 0
+
+    def do_GET(self):
+        content = self.do_GET_or_HEAD()
+        if content:
+            self.wfile.write(content)
+
+    def do_HEAD(self):
+        content = self.do_GET_or_HEAD()
+
+    def do_GET_or_HEAD(self):
+        self.path = urllib.unquote(self.path)
+        root = self.server.renderer.root
+        command = self.server.renderer.command
+        if self.path == '/':
+            if command:
+                return self.handle_command(command)
+            elif isinstance(root, str):
+                if os.path.isdir(root):
+                    return self.handle_dir(root)
+                else:
+                    return self.handle_rest_file(root)
+            else:
+                return self.handle_list(root)
+        elif self.path.startswith('/polling?'):
+            saved_atime = last_atime
+            self.path = urlparse.parse_qs(self.path.split('?', 1)[-1])['pathname'][0]
+            if self.path == '/':
+                self.path = os.path.basename(root)
+            self.server.renderer.root_mtime = os.stat(self.translate_path()).st_mtime
+            while last_atime == saved_atime:
+                if os.stat(self.translate_path()).st_mtime != self.server.renderer.root_mtime:
+                    try:
+                        self.send_response(200)
+                        self.send_header("Cache-Control", "no-cache, no-store, max-age=0")
+                        self.end_headers()
+                        self.server.renderer.root_mtime = os.stat(self.translate_path()).st_mtime
+                    except Exception, e:
+                        self.log_error('%s (client closed "%s" before acknowledgement)', e, self.path)
+                    finally:
+                        return
+                time.sleep(0.2)
+            try:
+                self.send_response(204)
+                self.end_headers()
+            except Exception, e:
+                self.log_error('%s (client closed "%s" before cancellation)', e, self.path)
+            finally:
+                return
+        elif '/..' in self.path:
+            self.send_error(400, "Bad request") # no hacking!
+        elif self.path.endswith('.gif'):
+            return self.handle_image(self.translate_path(), 'image/gif')
+        elif self.path.endswith('.png'):
+            return self.handle_image(self.translate_path(), 'image/png')
+        elif self.path.endswith('.jpg') or self.path.endswith('.jpeg'):
+            return self.handle_image(self.translate_path(), 'image/jpeg')
+        elif self.path.endswith('.txt') or self.path.endswith('.rst'):
+            return self.handle_rest_file(self.translate_path())
+        else:
+            self.send_error(501, 'File type not supported: %s' % self.path)
+
+    def translate_path(self):
+        root = self.server.renderer.root
+        path = self.path.lstrip('/')
+        if not isinstance(root, str):
+            (idx, path) = path.split('/', 1)
+            root = root[int(idx)]
+        if not os.path.isdir(root):
+            root = os.path.dirname(root)
+        return os.path.join(root, path)
+
+    def handle_image(self, filename, ctype):
+        try:
+            data = file(filename, 'rb').read()
+        except IOError:
+            self.send_error(404, 'File not found: %s' % self.path)
+        else:
+            self.send_response(200)
+            self.send_header("Content-Type", ctype)
+            self.send_header("Content-Length", str(len(data)))
+            self.end_headers()
+            return data
+
+    def handle_rest_file(self, filename):
+        try:
+            f = open(filename)
+            global last_atime
+            last_atime = time.time()
+            try:
+                return self.handle_rest_data(f.read())
+            finally:
+                f.close()
+        except IOError, e:
+            self.log_error('%s', e)
+            self.send_error(404, 'File not found: %s' % self.path)
+
+    def handle_command(self, command):
+        try:
+            f = os.popen(command)
+            try:
+                return self.handle_rest_data(f.read())
+            finally:
+                f.close()
+        except OSError, e:
+            self.log_error('%s' % e)
+            self.send_error(500, 'Command execution failed')
+
+    def handle_rest_data(self, data):
+        html = self.server.renderer.rest_to_html(data)
+        if isinstance(html, unicode):
+            html = html.encode('UTF-8')
+        self.send_response(200)
+        self.send_header("Content-Type", "text/html; charset=UTF-8")
+        self.send_header("Content-Length", str(len(html)))
+        self.send_header("Cache-Control", "no-cache, no-store, max-age=0")
+        self.end_headers()
+        return html
+
+    def collect_files(self, dirname):
+        if not dirname.endswith('/'):
+            dirname += '/'
+        files = []
+        for (dirpath, dirnames, filenames) in os.walk(dirname):
+            dirnames[:] = [dn for dn in dirnames
+                           if not dn.startswith('.')
+                           and not dn.endswith('.egg-info')]
+            for fn in filenames:
+                if fn.endswith('.txt') or fn.endswith('.rst'):
+                    prefix = dirpath[len(dirname):]
+                    files.append(os.path.join(prefix, fn))
+        files.sort(key=str.lower)
+        return files
+
+    def handle_dir(self, dirname):
+        files = [(fn, fn) for fn in self.collect_files(dirname)]
+        html = self.render_dir_listing('RST files in %s' % os.path.abspath(dirname), files)
+        if isinstance(html, unicode):
+            html = html.encode('UTF-8')
+        self.send_response(200)
+        self.send_header("Content-Type", "text/html; charset=UTF-8")
+        self.send_header("Content-Length", str(len(html)))
+        self.end_headers()
+        return html
+
+    def handle_list(self, list_of_files_or_dirs):
+        files = []
+        for (idx, fn) in enumerate(list_of_files_or_dirs):
+            if os.path.isdir(fn):
+                files.extend([(os.path.join(str(idx), f),
+                               os.path.join(fn, f))
+                               for f in self.collect_files(fn)])
+            else:
+                files.append((os.path.join(str(idx), os.path.basename(fn)),
+                              fn))
+        html = self.render_dir_listing('RST files', files)
+        if isinstance(html, unicode):
+            html = html.encode('UTF-8')
+        self.send_response(200)
+        self.send_header("Content-Type", "text/html; charset=UTF-8")
+        self.send_header("Content-Length", str(len(html)))
+        self.end_headers()
+        return html
+
+    def render_dir_listing(self, title, files):
+        files = ''.join([FILE_TEMPLATE.replace('$href', cgi.escape(href))
+                                      .replace('$file', cgi.escape(fn))
+                         for (href, fn) in files])
+        return (DIR_TEMPLATE.replace('$title', cgi.escape(title))
+                            .replace('$files', files))
+
+
+DIR_TEMPLATE = """\
+<!DOCTYPE html>
+<html>
+<head><title>$title</title></head>
+<body>
+<h1>$title</h1>
+<ul>
+$files</ul>
+</body>
+</html>
+"""
+
+FILE_TEMPLATE = """\
+  <li><a href="$href">$file</a></li>
+"""
+
+AJAX_STR = """
+<script type="text/javascript">
+var xmlHttp = null;
+window.onload = function () {
+    setTimeout(function () {
+        if (window.XMLHttpRequest) {
+            xmlHttp = new XMLHttpRequest();
+        } else if (window.ActiveXObject) {
+            xmlHttp = new ActiveXObject("Microsoft.XMLHTTP");
+        }
+        xmlHttp.onreadystatechange = function () {
+            if (xmlHttp.readyState == 4 && xmlHttp.status == '200') {
+                window.location.reload(true);
+            }
+        }
+        xmlHttp.open('HEAD', '/polling?pathname=' + location.pathname, true);
+        xmlHttp.send(null);
+    }, 0);
+}
+window.onbeforeunload = function () {
+    xmlHttp.abort();
+}
+</script>
+"""
+
+ERROR_TEMPLATE = """\
+<!DOCTYPE html>
+<html>
+<head><title>$title</title></head>
+<style type="text/css">
+pre.error {
+    border-left: 3px double red;
+    margin-left: 19px;
+    padding-left: 19px;
+    padding-top: 10px;
+    padding-bottom: 10px;
+    color: red;
+}
+</style>
+<body>
+<h1>$title</h1>
+<pre class="error">
+$error
+</pre>
+<pre>
+$source
+</pre>
+</body>
+</html>
+"""
+
+
+class ThreadingHTTPServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer):
+    daemon_threads = True
+
+    # tone down exception messages in the console
+    def handle_error(self, request, client_address):
+        print 'Exception detected during processing of request from',
+        print client_address
+
+
+class RestViewer(object):
+    """Web server that renders ReStructuredText on the fly."""
+
+    server_class = ThreadingHTTPServer
+    handler_class = MyRequestHandler
+
+    local_address = ('localhost', 0)
+
+    # only set one of those
+    css_url = None
+    css_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),
+                            'default.css')
+
+    def __init__(self, root, command=None):
+        self.root = root
+        self.command = command
+
+    def listen(self):
+        """Start listening on a TCP port.
+
+        Returns the port number.
+        """
+        self.server = self.server_class(self.local_address, self.handler_class)
+        self.server.renderer = self
+        return self.server.socket.getsockname()[1]
+
+    def serve(self):
+        """Wait for HTTP requests and serve them.
+
+        This function does not return.
+        """
+        self.server.serve_forever()
+
+    def rest_to_html(self, rest_input):
+        """Render ReStructuredText."""
+        writer = html4css1.Writer()
+        if pygments is not None:
+            writer.translator_class = SyntaxHighlightingHTMLTranslator
+        if self.css_url:
+            settings_overrides = {'stylesheet': self.css_url,
+                                  'stylesheet_path': None,
+                                  'embed_stylesheet': False}
+        elif self.css_path:
+            settings_overrides = {'stylesheet': self.css_path,
+                                  'stylesheet_path': None,
+                                  'embed_stylesheet': True}
+        else:
+            settings_overrides = {}
+        try:
+            core.publish_string(rest_input, writer=writer,
+                                settings_overrides=settings_overrides)
+        except Exception, e:
+            return self.render_exception(e.__class__.__name__, str(e),
+                                         rest_input)
+        return self.return_markup(writer.output)
+
+    def render_exception(self, title, error, source):
+        html = (ERROR_TEMPLATE.replace('$title', cgi.escape(title))
+                              .replace('$error', cgi.escape(error))
+                              .replace('$source', cgi.escape(source)))
+        return self.return_markup(html)
+
+    def return_markup(self, markup):
+        if self.command is None:
+            return markup.replace('</body>', AJAX_STR + '</body>')
+        else:
+            return markup.replace('</title>', cgi.escape(self.command) + '</title>')
+
+
+class SyntaxHighlightingHTMLTranslator(html4css1.HTMLTranslator):
+
+    in_doctest = False
+    in_text = False
+    in_reference = False
+    formatter_styles = formatters.HtmlFormatter(style='trac').get_style_defs('pre')
+
+    def __init__(self, document):
+        html4css1.HTMLTranslator.__init__(self, document)
+        self.body_prefix[:0] = ['<style type="text/css">\n', self.formatter_styles, '\n</style>\n']
+
+    def visit_doctest_block(self, node):
+        html4css1.HTMLTranslator.visit_doctest_block(self, node)
+        self.in_doctest = True
+
+    def depart_doctest_block(self, node):
+        html4css1.HTMLTranslator.depart_doctest_block(self, node)
+        self.in_doctest = False
+
+    def visit_Text(self, node):
+        if self.in_doctest:
+            text = node.astext()
+            lexer = lexers.PythonConsoleLexer()
+            formatter = formatters.HtmlFormatter(nowrap=True)
+            self.body.append(pygments.highlight(text, lexer, formatter))
+        else:
+            text = node.astext()
+            self.in_text = True
+            encoded = self.encode(text)
+            self.in_text = False
+            if self.in_mailto and self.settings.cloak_email_addresses:
+                encoded = self.cloak_email(encoded)
+            self.body.append(encoded)
+
+    def visit_literal(self, node):
+        self.in_text = True
+        html4css1.HTMLTranslator.visit_literal(self, node)
+        self.in_text = False
+
+    def visit_reference(self, node):
+        self.in_reference = True
+        html4css1.HTMLTranslator.visit_reference(self, node)
+
+    def depart_reference(self, node):
+        html4css1.HTMLTranslator.depart_reference(self, node)
+        self.in_reference = False
+
+    def encode(self, text):
+        encoded = html4css1.HTMLTranslator.encode(self, text)
+        if self.in_text and not self.in_reference:
+            encoded = self.link_local_files(encoded)
+        return encoded
+
+    @staticmethod
+    def link_local_files(text):
+        """Replace filenames with hyperlinks.
+
+            >>> link_local_files = SyntaxHighlightingHTMLTranslator.link_local_files
+            >>> link_local_files('e.g. see README.txt for more info')
+            'e.g. see <a href="README.txt">README.txt</a> for more info'
+
+        """
+        # jwz was right...
+        text = re.sub("([-_a-zA-Z0-9]+[.]txt)", r'<a href="\1">\1</a>', text)
+        return text
+
+
+def parse_address(addr):
+    """Parse a socket address.
+
+        >>> parse_address('1234')
+        ('localhost', 1234)
+
+        >>> parse_address('example.com:1234')
+        ('example.com', 1234)
+
+        >>> parse_address('*:1234')
+        ('', 1234)
+
+        >>> try: parse_address('notanumber')
+        ... except ValueError, e: print e
+        Invalid address: notanumber
+
+        >>> try: parse_address('la:la:la')
+        ... except ValueError, e: print e
+        Invalid address: la:la:la
+
+    """
+    if ':' in addr:
+        try:
+            (host, port) = addr.split(':')
+        except ValueError:
+            raise ValueError('Invalid address: %s' % addr)
+    else:
+        (host, port) = ('localhost', addr)
+    if host == '*':
+        host = '' # any
+    try:
+        return (host, int(port))
+    except ValueError:
+        raise ValueError('Invalid address: %s' % addr)
+
+
+def get_host_name(listen_on):
+    """Convert a listening interface name to a host name.
+
+    The important part is to convert 0.0.0.0 to the system hostname, everything
+    else can be left as is.
+    """
+    try:
+        ip_addr = socket.inet_aton(listen_on)
+    except socket.error: # probably a hostname or ''
+        ip_addr = None
+    if listen_on == '' or ip_addr == '\0\0\0\0':
+        return socket.gethostname()
+    else:
+        return listen_on
+
+
+def main():
+    progname = os.path.basename(sys.argv[0])
+    parser = optparse.OptionParser("%prog [options] filename-or-directory [...]",
+                    description="Serve ReStructuredText files over HTTP.",
+                    prog=progname)
+    parser.add_option('-l', '--listen',
+                      help='listen on a given port (or interface:port,'
+                           ' e.g. *:8080) [default: random port on localhost]',
+                      default=None)
+    parser.add_option('-b', '--browser',
+                      help='open a web browser [default: only if -l'
+                           ' was not specified]',
+                      action='store_true', default=None)
+    parser.add_option('-e', '--execute',
+                      help='run a command to produce ReStructuredText',
+                      default=None)
+    parser.add_option('--long-description',
+                      help='run "python setup.py --long-description" to produce ReStructuredText',
+                      action='store_const', dest='execute',
+                      const='python setup.py --long-description')
+    parser.add_option('--css',
+                      help='use the specified stylesheet',
+                      action='store', dest='css_path', default=None)
+    (opts, args) = parser.parse_args(sys.argv[1:])
+    if not args and not opts.execute:
+        parser.error("at least one argument expected")
+    if args and opts.execute:
+        parser.error("specify a command (-e) or a file/directory, but not both")
+    if opts.browser is None:
+        opts.browser = opts.listen is None
+    if opts.execute:
+        server = RestViewer('.', command=opts.execute)
+    elif len(args) == 1:
+        server = RestViewer(args[0])
+    else:
+        server = RestViewer(args)
+    if opts.css_path:
+        if (opts.css_path.startswith('http://') or
+            opts.css_path.startswith('https://')):
+            server.css_url = opts.css_path
+            server.css_path = None
+        else:
+            server.css_path = opts.css_path
+            server.css_url = None
+    if opts.listen:
+        try:
+            server.local_address = parse_address(opts.listen)
+        except ValueError, e:
+            sys.exit(str(e))
+    host = get_host_name(server.local_address[0])
+    port = server.listen()
+    url = 'http://%s:%d/' % (host, port)
+    print "Listening on %s" % url
+    if opts.browser:
+        # launch the web browser in the background as it may block
+        t = threading.Thread(target=webbrowser.open, args=(url, ))
+        t.setDaemon(True)
+        t.start()
+    try:
+        server.serve()
+    except KeyboardInterrupt:
+        pass
+
+
+if __name__ == '__main__':
+    main()
+

python/restview/test.py

+#!/usr/bin/python
+import unittest
+import doctest
+
+def test_suite():
+    return doctest.DocTestSuite('restview.restviewhttp')
+
+if __name__ == '__main__':
+    import sys, os
+    sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
+    unittest.main(defaultTest='test_suite')