Index: django/core/management/commands/htmldocs.py
===================================================================
--- django/core/management/commands/htmldocs.py	(revision 0)
+++ django/core/management/commands/htmldocs.py	(revision 0)
@@ -0,0 +1,296 @@
+"""
+Command that converts Django documentation from ReST to HTML.
+
+Based on django_website/apps/docs/builder.py.
+"""
+
+# TODO:
+# 1. fix inter-page links
+# 2. fetch external images and place them to img directory, fix image links
+# 3. add images used in CSS (in notes etc)
+# 4. add PDF support, take inspiration from http://code.google.com/p/rst2pdf/
+
+import os
+import shutil
+from optparse import make_option
+from django.core.management import BaseCommand, CommandError
+
+CSS_FILE = "docs.css"
+
+class Command(BaseCommand):
+    option_list = BaseCommand.option_list + (
+            make_option('--single', action='store_true', dest='single',
+                default=False, help = "Write a single combined HTML file. "
+                "Not recommended, produces output that is harder to navigate."),
+    )
+    help = ("Converts Django documentation to HTML. Uses the given Django "
+            "documentation directory for reading documentation source and "
+            "output directory for writing HTML files. Creates the output "
+            "directory if it does not exist.")
+    args = "[Django documentation directory] [output directory]"
+
+    requires_model_validation = False
+    can_import_settings = False
+
+    def handle(self, *paths, **options):
+        try:
+            from docutils.core import publish_parts
+        except ImportError:
+            raise CommandError("The docutils module is required to run "
+                    "this command.")
+
+        if len(paths) != 2:
+            raise CommandError("Please provide exactly two arguments in the "
+                    "following order: %s." % self.args)
+
+        source, dest = paths
+        if not os.path.exists(dest):
+            try:
+                os.mkdir(dest)
+            except OSError, e:
+                raise CommandError("Failed to create directory '%s'. "
+                        "The error was '%s'." % (dest, e))
+        for p in source, dest:
+            if not os.path.isdir(p):
+                raise CommandError("'%s' is not a directory." % p)
+
+        css_path = os.path.join(source, "css", CSS_FILE)
+        if not os.path.exists(css_path):
+            raise CommandError("'%s' does not appear to be a Django "
+                    "documentation directory (stylesheet not found)" % source)
+        try:
+            shutil.copy(css_path, dest)
+        except IOError, e:
+            raise CommandError("Unable to write to directory '%s'. "
+                    "The error was '%s'." % (dest, e))
+
+        opt_single = options.get("single", False)
+
+        files = [f for f in os.listdir(source) if f.endswith('.txt')]
+        files.sort()
+
+        if opt_single:
+            docs = []
+        index = []
+        for file in files:
+            out = file[:-4] + ".html"
+            print "Converting '%s'... " % out,
+            doc = publish_parts(open(os.path.join(source, file)).read(),
+                    writer=get_django_html_writer(opt_single),
+                    settings_overrides={'initial_header_level': 2})
+            print "done"
+            try:
+                index.append([out, doc['title']])
+            except KeyError:
+                index.append([out, 'UNKNOWN (%s)' % file])
+        
+            if opt_single:
+                doc['id'] = out
+                docs.append(doc)
+            else:
+                out_path = os.path.join(dest, out)
+                print "Writing '%s'... " % out_path,
+                open(out_path, 'w').write(render_doc(doc))
+                print "done"
+        
+        # write out either index or the single combined page
+        doc = None
+        if opt_single:
+            doc = {
+                'title' : "Documentation",
+                'toc' : get_single_toc(index),
+                'body' : get_single_body(docs)
+            }
+        else:
+            doc = {
+                'title' : "Documentation index",
+                'toc' : "<p>This is the automatically genrated documentation "
+                            "index. The automatic documentation is a basic "
+                            "reference, more resources are available on the "
+                            '<a href="http://www.djangoproject.com/documentation/">'
+                            "Django website</a>.</p>",
+                'body' : '<ul>%s</ul>' % \
+                        '\n'.join(['<li><a href="%s">%s</a></li>' \
+                            % (href, title) for href, title in index ])
+            }
+        print "Writing index... ",
+        open(os.path.join(dest, "index.html"), 'w').write(render_doc(doc))
+        print "done"
+        print "All done"
+
+def get_single_toc(index):
+    """
+    Creates the table of contents for single-document output.
+    """
+    toc = '<ul class="toc">%s</ul>'
+    line = '<li><a class="reference internal" href="#%s">%s</a></li>'
+    return toc % '\n'.join([ line % (name, title) for name, title in index])
+    
+def get_single_body(docs):
+    """
+    Joins individual documents into a single document.
+    """
+    chunk = '<h1 id="%(id)s">%(title)s</h1>\n%(body)s'
+    return '\n'.join([chunk % doc for doc in docs])
+
+def render_doc(doc):
+    """
+    Renders the document with a HTML template.
+    """
+    return ("""<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+	"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+	<head>
+		<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
+		<meta http-equiv="Content-Language" content="en-us" />
+		<title>%(title)s | Django Documentation</title>
+		<link href="docs.css" rel="stylesheet" type="text/css" media="screen" />
+    </head>
+    <body id="documentation" class="default">
+    <div id="container">
+        <div id="header">
+            <ul id="nav-global">
+                <li id="nav-homepage"><a href="index.html">Documentation index</a></li>
+            </ul>
+        </div>
+        <div id="billboard"><h2><a href="index.html" style="color: white; height:40px; padding-top:20px; text-indent:22px;">Django documentation</a></h2></div>
+        <div id="columnwrap">
+            <div id="content-main">
+                <h1>%(title)s</h1>
+                %(body)s
+            </div>
+            <div id="content-related" class="sidebar">
+                <h2>Contents</h2>
+                %(toc)s
+            </div>
+        </div>
+        <div id="footer">
+            <p><a href="index.html"><< Back to documentation index</a></p>
+        </div>
+    </div>
+    </body>
+</html>""" % doc).encode('utf-8')
+
+def get_django_html_writer(is_single):
+    """
+    Returns the Django HTML writer instance. Note that we need to define the
+    classes inside a function to avoid triggering an import error when
+    docutils is unavailable.
+    """
+    from docutils import nodes
+    from docutils.writers import html4css1
+    import re
+
+    class DjangoHTMLTranslator(html4css1.HTMLTranslator):
+        """
+        reST -> HTML translator subclass that outputs Django-specific markup.
+        """
+        
+        # Prevent name attributes from being generated
+        named_tags = []
+        
+        def __init__(self, document):
+            html4css1.HTMLTranslator.__init__(self, document)
+            self._in_literal = 0
+        
+        # Remove the default border=1 from <table>
+        def visit_table(self, node):
+            self.body.append(self.starttag(node, 'table', CLASS='docutils'))
+
+        # No smartypants conversion
+
+        # Avoid <blockquote>s around merely indented nodes.
+        # Adapted from
+        # http://thread.gmane.org/gmane.text.docutils.user/742/focus=804
+        
+        _suppress_blockquote_child_nodes = (
+            nodes.bullet_list, nodes.enumerated_list, nodes.definition_list,
+            nodes.literal_block, nodes.doctest_block, nodes.line_block,
+            nodes.table
+        )
+        def _bq_is_valid(self, node):
+            return len(node.children) != 1 \
+                    or not isinstance(node.children[0],
+                            self._suppress_blockquote_child_nodes)
+                                            
+        def visit_block_quote(self, node):
+            if self._bq_is_valid(node):
+                html4css1.HTMLTranslator.visit_block_quote(self, node)
+
+        def depart_block_quote(self, node):
+            if self._bq_is_valid(node):
+                html4css1.HTMLTranslator.depart_block_quote(self, node)
+
+    # Fix inter-page links
+
+    class MultiPageTranslator(DjangoHTMLTranslator):
+        def visit_reference(self, node, regex=re.compile(r'^../([-\w]+)/')):
+            if node.has_key('refuri') and regex.match(node['refuri']):
+                node['refuri'] = regex.sub(r'\1.html', node['refuri'])
+            html4css1.HTMLTranslator.visit_reference(self, node)
+
+    class SinglePageTranslator(DjangoHTMLTranslator):
+        # Destroys ../foo/#bar style anchors (replacing them with #foo),
+        # but we can live with that
+        def visit_reference(self, node, regex=re.compile(r'^../([-\w]+)/.*')):
+            if node.has_key('refuri') and regex.match(node['refuri']):
+                node['refuri'] = regex.sub(r'#\1.html', node['refuri'])
+            html4css1.HTMLTranslator.visit_reference(self, node)
+
+    class DjangoHTMLWriter(html4css1.Writer):
+        """
+        HTML writer that adds a "toc" key to the set of doc parts.
+        """
+        def __init__(self):
+            html4css1.Writer.__init__(self)
+            if is_single:
+                self.translator_class = SinglePageTranslator
+            else:
+                self.translator_class = MultiPageTranslator
+
+        def translate(self):
+            # build the document
+            html4css1.Writer.translate(self)
+
+            # build the contents
+            contents = self.build_contents(self.document)
+            contents_doc = self.document.copy()
+            contents_doc.children = contents
+            contents_visitor = self.translator_class(contents_doc)
+            contents_doc.walkabout(contents_visitor)
+            self.parts['toc'] = "<ul class='toc'>%s</ul>" \
+                    % ''.join(contents_visitor.fragment)
+
+        def build_contents(self, node, level=0):
+            level += 1
+            sections = []
+            i = len(node) - 1
+            while i >= 0 and isinstance(node[i], nodes.section):
+                sections.append(node[i])
+                i -= 1
+            sections.reverse()
+            entries = []
+            autonum = 0
+            depth = 4   # XXX FIXME
+            for section in sections:
+                title = section[0]
+                entrytext = title
+                try:
+                    reference = nodes.reference('', '',
+                            refid=section['ids'][0], *entrytext)
+                except IndexError:
+                    continue
+                ref_id = self.document.set_id(reference)
+                entry = nodes.paragraph('', '', reference)
+                item = nodes.list_item('', entry)
+                if level < depth:
+                    subsects = self.build_contents(section, level)
+                    item += subsects
+                entries.append(item)
+            if entries:
+                contents = nodes.bullet_list('', *entries)
+                return contents
+            else:
+                return []
+
+    return DjangoHTMLWriter()
Index: docs/css/docs.css
===================================================================
--- docs/css/docs.css	(revision 0)
+++ docs/css/docs.css	(revision 0)
@@ -0,0 +1,200 @@
+/* 
+djangoproject.com by Wilson Miner (wilson@lawrence.com)
+Copyright (c) 2005 Lawrence Journal-World. Please don't steal.
+*/
+
+
+/* SETUP */
+
+body { margin:0; padding:0; background:#092e20; color:white; }
+body, th, td { font:12px/1.4em Verdana,sans-serif; }
+#container { position:relative; min-width:55em; max-width:100em; }
+#homepage #container { max-width:100em; }
+
+/* LINKS */
+
+a {text-decoration: none;}
+a img {border: none;}
+a:link, a:visited { color:#ffc757; }
+#content-main a:link, #content-main a:visited { color:#ab5603; text-decoration:underline; }
+#content-secondary a:link, #content-secondary a:visited { color:#ffc757; text-decoration:none; }
+a:hover { color:#ffe761; }
+#content-main a:hover { background-color:#E0FFB8; color:#234f32; text-decoration:none; }
+#content-secondary a:hover { color:#ffe761; background:none; }
+#content-main h2 a, #content-main h3 a { text-decoration:none !important; }
+
+/* HEADER */
+
+#header { position:relative; height:6.5em; background:#092e20; }
+#header h1#logo { margin:0; width:111px; height:41px; position:absolute; bottom:10px; left:25px; }
+
+/* NAV */
+
+#nav-global { position:absolute; margin:0; bottom:0; right:0; font-family:"Trebuchet MS",sans-serif; white-space:nowrap; }
+#nav-global li { display:block; float:left; list-style-type:none; margin:0; padding:0; }
+#nav-global a { display:block; float:left; padding:5em 16px 10px 16px; background:#092e20; }
+#nav-global a:hover { color:white; background:#234f32; }
+#homepage #nav-homepage a, #overview #nav-overview a, #download #nav-download a, #documentation #nav-documentation a, #weblog #nav-weblog a, #community #nav-community a, #blogroll #nav-blogroll a, #code #nav-code a { color:white; background:#092e20 url(../img/site/nav_bg.gif) bottom repeat-x; }
+
+/* COLUMNS */
+
+#columnwrap { background:#234f32; padding-bottom:10px; }
+#subwrap { background:#326342; width:73%; float:left; padding-bottom:10px; }
+#content-main { float:left; width:70%; background:white; color:black; padding-bottom:10px; }
+#generic #content-main, #code #content-main { width:100%; }
+#content-main * { margin-left:22px; margin-right:24px; }
+#content-main * * { margin-left:0; margin-right:0; }
+.sidebar { font-size:92%; }
+.sidebar * { margin-left:14px; margin-right:14px; }
+.sidebar * * { margin-left:0; margin-right:0; }
+#content-extra { float:right; width:27%; }
+#content-related { float:right; width:30%;}
+#content-secondary { clear:both; background:#487858; margin-left:0; margin-right:0; margin-top:15px; margin-bottom:-10px; padding:10px 24px; color:white; }
+.subcol-primary, .subcol-secondary { width:40%; float:left; padding-bottom:1.2em; }
+.subcol-primary { margin-right:1%; }
+
+/* CONTENT */
+
+h1,h2,h3 { margin-top:.8em; font-family:"Trebuchet MS",sans-serif; font-weight:normal; }
+h1 { font-size:218%; margin-top:.6em; margin-bottom:.6em; color:#092e20; line-height:1.1em; }
+h2 { font-size:150%; margin-top:1em; margin-bottom:.2em; line-height:1.2em; color:#092e20; }
+#homepage h2 { font-size:140%; }
+h3 { font-size:125%; font-weight:bold; margin-bottom:.2em; color:#487858; }
+h4 { font-size:100%; font-weight:bold; margin-bottom:-3px; margin-top:1.2em; text-transform:uppercase; letter-spacing:1px; }
+h4 pre, h4 tt, h4 .literal { text-transform:none; }
+h5 { font-size:1em; font-weight:bold; margin-top:1.5em; margin-bottom:3px; }
+p, ul, dl { margin-top:.6em; margin-bottom:.8em; }
+hr { color:#ccc; background-color:#ccc; height:1px; border:0; }
+p.date { color:#487858; margin-top:-.2em; }
+p.more { margin-top:-.4em; }
+.sidebar p.date { color:#90ba9e; }
+#content-secondary h2, .sidebar h2 { color:white; }
+#content-secondary h3, .sidebar h3 { color:#9aef3f; }
+#content-secondary h2:first-child { margin-top:.6em; }
+.sidebar h2:first-child { margin-top:.8em; }
+#content-main h2, #content-main h3 { margin-top:1.2em; }
+h2.deck { margin-top:-.5em !important; margin-bottom:.6em; color:#487858; }
+ins { text-decoration: none; }
+ins a { text-decoration: none; }
+
+/* LISTS */
+
+ul { padding-left:2em; }
+ol { padding-left:30px; }
+ul li { list-style-type:square; margin-bottom:.4em; }
+ul ul { padding-left:1.2em; }
+ul ul ul { padding-left:1em; }
+ul.linklist, ul.toc { padding-left:0; }
+ul.toc ul { margin-left:.6em; }
+ul.toc ul li { list-style-type:square; }
+ul.toc ul ul li { list-style-type:disc; }
+ul.linklist li, ul.toc li { list-style-type:none; }
+dt { font-weight:bold; margin-top:.5em; font-size:1.1em; }
+dd { margin-bottom:.8em; }
+
+/*  RSS  */
+
+a.rss { font:bold 10px Verdana, sans-serif; padding:0 .2em; border: 1px solid; text-decoration:none; background:#f60;color: #fff; border-color:#ffc8a4 #7d3302 #3f1a01 #ff9a57; margin:0 3px; vertical-align:middle; }
+#content-main a.rss { color:#fff; text-decoration:none; }
+a.rss:hover, a.rss:link, a.rss:visited { color:#fff; text-decoration:none; }
+
+/* BLOCKQUOTES */
+
+#weblog blockquote { padding-left:0.8em; padding-right:1em; font:125%/1.2em "Trebuchet MS", sans-serif; color:#234f32; border-left:2px solid #94da3a; }
+.sidebar blockquote { margin-top:1.5em; margin-bottom:1.5em; }
+.sidebar blockquote p { font:italic 175%/1.2em "Trebuchet MS",sans-serif; color:#94da3a; }
+.sidebar blockquote cite { display:block; font-style:normal; line-height:1.2em; margin-top:-.8em; color:#94da3a; }
+.sidebar cite strong { font-weight:normal; color:white; }
+
+/* CODE BLOCKS */
+
+.literal { white-space:nowrap; }
+.literal, .literal-block { color:#234f32; }
+.sidebar .literal { color:white; background:transparent; font-size:11px; }
+pre, .literal-block { font-size:medium; background:#E0FFB8; border:1px solid #94da3a; border-width:1px 0; margin: 1em 0; padding: .3em .4em; overflow: auto; }
+dt .literal, table .literal { background:none; }
+textarea.codedump { font-size:10px; color:#234f32; width:100%; background:#E0FFB8; border:1px solid #94da3a; border-width:1px 0; padding: .3em .4em; }
+
+/* NOTES & ADMONITIONS */
+
+.note, .admonition, .caution { padding:.8em 1em .8em; margin: 1em 0; border:1px solid #94da3a; }
+.admonition-title { font-weight:bold; margin-top:0 !important; margin-bottom:0 !important;}
+.admonition .last { margin-bottom:0 !important; }
+.admonition-philosophy { padding-left:65px; background:url(../img/doc/icons/docicons-philosophy.gif) .8em .8em no-repeat;}
+.admonition-note, .caution { padding-left:65px; background:url(../img/doc/icons/docicons-note.gif) .8em .8em no-repeat;}
+.admonition-behind-the-scenes { padding-left:65px; background:url(../img/doc/icons/docicons-behindscenes.gif) .8em .8em no-repeat;}
+
+/* DOCS */
+
+#documentation h2, #documentation h3, #documentation h4 { margin-top:1.4em; }
+#documentation dd { margin-left:1em; }
+#content-main table { color:#000; }
+table.docutils { border-collapse:collapse; }
+table.docutils thead th { border-bottom:2px solid #dfdfdf; text-align:left; }
+table.docutils td, table.docutils th { border-bottom:1px solid #dfdfdf; padding:4px 2px;}
+table.docutils td p { margin-top:0; margin-bottom:.5em; }
+#documentation #content-related .literal { background:transparent !important; }
+
+/* BILLBOARDS */
+
+#billboard { background:#94da3a url(../img/site/bbdsm_bg.gif) repeat-x; border-bottom:6px solid #092e20; }
+#billboard h2 { margin:0; }
+#generic #billboard { display:none; }
+#homepage #billboard { background-image: url(../img/site/bbd_bg.gif); }
+#homepage #billboard h2 { margin:0; text-indent:-5000px; height:80px; width:633px; background:url(../img/site/bbd_homepage.gif) no-repeat; }
+#overview #billboard h2 { margin:0; text-indent:-5000px; height:60px; width:203px; background:url(../img/site/bbd_overview.gif) no-repeat; }
+#download #billboard h2 { margin:0; text-indent:-5000px; height:60px; width:203px; background:url(../img/site/bbd_download.gif) no-repeat; }
+#documentation #billboard h2 a { display:block; margin:0; text-indent:-5000px; height:60px; width:226px; background:url(../img/site/bbd_documentation.gif) no-repeat; }
+#weblog #billboard h2 a { display:block; margin:0; text-indent:-5000px; height:60px; width:226px; background:url(../img/site/bbd_weblog.gif) no-repeat; }
+#community #billboard h2 { display:block; margin:0; text-indent:-5000px; height:60px; width:226px; background:url(../img/site/bbd_community.gif) no-repeat; }
+#blogroll #billboard h2 { display:block; margin:0; text-indent:-5000px; height:60px; width:168px; background:url(../img/site/bbd_blogroll.gif) no-repeat; }
+#code #billboard h2 a { display:block; margin:0; text-indent:-5000px; height:60px; width:184px; background:url(../img/site/bbd_code.gif) no-repeat; }
+
+/* FOOTER */
+
+#footer { clear:both; color:#487858; padding:10px 20px; font-size:90%; }
+
+/* COMMENTS */
+
+.comment { margin:15px 0; }
+div.comment p { margin-left:1em; }
+#weblog div.comment p.date { margin-bottom:.2em; color:#94da3a; }
+
+/* MISC */
+
+.small { font-size:90%; }
+h3 .small { font-size:80%; }
+.quiet { font-weight:normal; }
+.clear { clear:both; }
+#content-main .quiet { color:#487858; }
+#content-secondary .quiet { color:#90ba9e; }
+
+/*  CLEARFIX KLUDGE */
+
+#columnwrap:after {
+    content: "."; 
+    display: block; 
+    height: 0; 
+    clear: both; 
+    visibility: hidden;
+}
+#columnwrap { display: inline-block; }
+
+/* Hides from IE-mac \*/
+* html #columnwrap { height: 1%; }
+#columnwrap { display: block; }
+/* End hide from IE-mac */
+
+#subwrap:after {
+    content: "."; 
+    display: block; 
+    height: 0; 
+    clear: both; 
+    visibility: hidden;
+}
+#subwrap { display: inline-block; }
+
+/* Hides from IE-mac \*/
+* html #subwrap { height: 1%; }
+#subwrap { display: block; }
+/* End hide from IE-mac */om IE-mac */
Index: docs/django-admin.txt
===================================================================
--- docs/django-admin.txt	(revision 7321)
+++ docs/django-admin.txt	(working copy)
@@ -199,6 +199,24 @@
 
     django-admin.py flush --verbosity=2
 
+htmldocs docdir outdir
+----------------------
+
+Converts Django documentation to HTML. Uses the given Django documentation
+directory ``docdir`` for reading documentation source and output directory
+``outdir`` (creating it if necessary) for writing HTML files. Writes multiple
+HTML files, i.e. one HTML file per documentation file by default.
+
+--single
+~~~~~~~~~~~
+
+Creates a single combined HTML file. Not recommended, produces output that is
+harder to navigate and more than 1.8 MB large.
+
+Example usage::
+
+    django-admin.py htmldocs trunk/docs django_docs --single
+
 inspectdb
 ---------
 
