summaryrefslogtreecommitdiff
path: root/sphinx/builders
diff options
context:
space:
mode:
Diffstat (limited to 'sphinx/builders')
-rw-r--r--sphinx/builders/__init__.py384
-rw-r--r--sphinx/builders/changes.py156
-rw-r--r--sphinx/builders/html.py836
-rw-r--r--sphinx/builders/htmlhelp.py250
-rw-r--r--sphinx/builders/latex.py203
-rw-r--r--sphinx/builders/linkcheck.py133
-rw-r--r--sphinx/builders/qthelp.py263
-rw-r--r--sphinx/builders/text.py70
8 files changed, 2295 insertions, 0 deletions
diff --git a/sphinx/builders/__init__.py b/sphinx/builders/__init__.py
new file mode 100644
index 00000000..41f63de4
--- /dev/null
+++ b/sphinx/builders/__init__.py
@@ -0,0 +1,384 @@
+# -*- coding: utf-8 -*-
+"""
+ sphinx.builders
+ ~~~~~~~~~~~~~~~
+
+ Builder superclass for all builders.
+
+ :copyright: Copyright 2007-2009 by the Sphinx team, see AUTHORS.
+ :license: BSD, see LICENSE for details.
+"""
+
+import os
+import gettext
+from os import path
+
+from docutils import nodes
+
+from sphinx import package_dir, locale
+from sphinx.util import SEP, relative_uri
+from sphinx.environment import BuildEnvironment
+from sphinx.util.console import bold, purple, darkgreen, term_width_line
+
+# side effect: registers roles and directives
+from sphinx import roles
+from sphinx import directives
+
+
+ENV_PICKLE_FILENAME = 'environment.pickle'
+
+
+class Builder(object):
+ """
+ Builds target formats from the reST sources.
+ """
+
+ # builder's name, for the -b command line options
+ name = ''
+ # builder's output format, or '' if no document output is produced
+ format = ''
+
+ def __init__(self, app, env=None, freshenv=False):
+ self.srcdir = app.srcdir
+ self.confdir = app.confdir
+ self.outdir = app.outdir
+ self.doctreedir = app.doctreedir
+ if not path.isdir(self.doctreedir):
+ os.makedirs(self.doctreedir)
+
+ self.app = app
+ self.warn = app.warn
+ self.info = app.info
+ self.config = app.config
+
+ self.load_i18n()
+
+ # images that need to be copied over (source -> dest)
+ self.images = {}
+
+ # if None, this is set in load_env()
+ self.env = env
+ self.freshenv = freshenv
+
+ self.init()
+ self.load_env()
+
+ # helper methods
+
+ def init(self):
+ """
+ Load necessary templates and perform initialization. The default
+ implementation does nothing.
+ """
+ pass
+
+ def create_template_bridge(self):
+ """
+ Return the template bridge configured.
+ """
+ if self.config.template_bridge:
+ self.templates = self.app.import_object(
+ self.config.template_bridge, 'template_bridge setting')()
+ else:
+ from sphinx.jinja2glue import BuiltinTemplateLoader
+ self.templates = BuiltinTemplateLoader()
+
+ def get_target_uri(self, docname, typ=None):
+ """
+ Return the target URI for a document name (*typ* can be used to qualify
+ the link characteristic for individual builders).
+ """
+ raise NotImplementedError
+
+ def get_relative_uri(self, from_, to, typ=None):
+ """
+ Return a relative URI between two source filenames. May raise
+ environment.NoUri if there's no way to return a sensible URI.
+ """
+ return relative_uri(self.get_target_uri(from_),
+ self.get_target_uri(to, typ))
+
+ def get_outdated_docs(self):
+ """
+ Return an iterable of output files that are outdated, or a string
+ describing what an update build will build.
+
+ If the builder does not output individual files corresponding to
+ source files, return a string here. If it does, return an iterable
+ of those files that need to be written.
+ """
+ raise NotImplementedError
+
+ def old_status_iterator(self, iterable, summary, colorfunc=darkgreen):
+ l = 0
+ for item in iterable:
+ if l == 0:
+ self.info(bold(summary), nonl=1)
+ l = 1
+ self.info(colorfunc(item) + ' ', nonl=1)
+ yield item
+ if l == 1:
+ self.info()
+
+ # new version with progress info
+ def status_iterator(self, iterable, summary, colorfunc=darkgreen, length=0):
+ if length == 0:
+ for item in self.old_status_iterator(iterable, summary, colorfunc):
+ yield item
+ return
+ l = 0
+ summary = bold(summary)
+ for item in iterable:
+ l += 1
+ self.info(term_width_line('%s[%3d%%] %s' %
+ (summary, 100*l/length,
+ colorfunc(item))), nonl=1)
+ yield item
+ if l > 0:
+ self.info()
+
+ supported_image_types = []
+
+ def post_process_images(self, doctree):
+ """
+ Pick the best candidate for all image URIs.
+ """
+ for node in doctree.traverse(nodes.image):
+ if '?' in node['candidates']:
+ # don't rewrite nonlocal image URIs
+ continue
+ if '*' not in node['candidates']:
+ for imgtype in self.supported_image_types:
+ candidate = node['candidates'].get(imgtype, None)
+ if candidate:
+ break
+ else:
+ self.warn(
+ 'no matching candidate for image URI %r' % node['uri'],
+ '%s:%s' % (node.source, getattr(node, 'line', '')))
+ continue
+ node['uri'] = candidate
+ else:
+ candidate = node['uri']
+ if candidate not in self.env.images:
+ # non-existing URI; let it alone
+ continue
+ self.images[candidate] = self.env.images[candidate][1]
+
+ # build methods
+
+ def load_i18n(self):
+ """
+ Load translated strings from the configured localedirs if
+ enabled in the configuration.
+ """
+ self.translator = None
+ if self.config.language is not None:
+ self.info(bold('loading translations [%s]... ' %
+ self.config.language), nonl=True)
+ locale_dirs = [path.join(package_dir, 'locale')] + \
+ [path.join(self.srcdir, x) for x in self.config.locale_dirs]
+ for dir_ in locale_dirs:
+ try:
+ trans = gettext.translation('sphinx', localedir=dir_,
+ languages=[self.config.language])
+ if self.translator is None:
+ self.translator = trans
+ else:
+ self.translator._catalog.update(trans.catalog)
+ except Exception:
+ # Language couldn't be found in the specified path
+ pass
+ if self.translator is not None:
+ self.info('done')
+ else:
+ self.info('locale not available')
+ if self.translator is None:
+ self.translator = gettext.NullTranslations()
+ self.translator.install(unicode=True)
+ locale.init() # translate common labels
+
+ def load_env(self):
+ """Set up the build environment."""
+ if self.env:
+ return
+ if not self.freshenv:
+ try:
+ self.info(bold('loading pickled environment... '), nonl=True)
+ self.env = BuildEnvironment.frompickle(self.config,
+ path.join(self.doctreedir, ENV_PICKLE_FILENAME))
+ self.info('done')
+ except Exception, err:
+ if type(err) is IOError and err.errno == 2:
+ self.info('not found')
+ else:
+ self.info('failed: %s' % err)
+ self.env = BuildEnvironment(self.srcdir, self.doctreedir,
+ self.config)
+ self.env.find_files(self.config)
+ else:
+ self.env = BuildEnvironment(self.srcdir, self.doctreedir,
+ self.config)
+ self.env.find_files(self.config)
+ self.env.set_warnfunc(self.warn)
+
+ def build_all(self):
+ """Build all source files."""
+ self.build(None, summary='all source files', method='all')
+
+ def build_specific(self, filenames):
+ """Only rebuild as much as needed for changes in the *filenames*."""
+ # bring the filenames to the canonical format, that is,
+ # relative to the source directory and without source_suffix.
+ dirlen = len(self.srcdir) + 1
+ to_write = []
+ suffix = self.config.source_suffix
+ for filename in filenames:
+ filename = path.abspath(filename)[dirlen:]
+ if filename.endswith(suffix):
+ filename = filename[:-len(suffix)]
+ filename = filename.replace(os.path.sep, SEP)
+ to_write.append(filename)
+ self.build(to_write, method='specific',
+ summary='%d source files given on command '
+ 'line' % len(to_write))
+
+ def build_update(self):
+ """Only rebuild what was changed or added since last build."""
+ to_build = self.get_outdated_docs()
+ if isinstance(to_build, str):
+ self.build(['__all__'], to_build)
+ else:
+ to_build = list(to_build)
+ self.build(to_build,
+ summary='targets for %d source files that are '
+ 'out of date' % len(to_build))
+
+ def build(self, docnames, summary=None, method='update'):
+ """
+ Main build method. First updates the environment, and then
+ calls :meth:`write`.
+ """
+ if summary:
+ self.info(bold('building [%s]: ' % self.name), nonl=1)
+ self.info(summary)
+
+ updated_docnames = set()
+ # while reading, collect all warnings from docutils
+ warnings = []
+ self.env.set_warnfunc(lambda *args: warnings.append(args))
+ self.info(bold('updating environment: '), nonl=1)
+ msg, length, iterator = self.env.update(self.config, self.srcdir,
+ self.doctreedir, self.app)
+ self.info(msg)
+ for docname in self.status_iterator(iterator, 'reading sources... ',
+ purple, length):
+ updated_docnames.add(docname)
+ # nothing further to do, the environment has already
+ # done the reading
+ for warning in warnings:
+ self.warn(*warning)
+ self.env.set_warnfunc(self.warn)
+
+ doccount = len(updated_docnames)
+ self.info(bold('looking for now-outdated files... '), nonl=1)
+ for docname in self.env.check_dependents(updated_docnames):
+ updated_docnames.add(docname)
+ outdated = len(updated_docnames) - doccount
+ if outdated:
+ self.info('%d found' % outdated)
+ else:
+ self.info('none found')
+
+ if updated_docnames:
+ # save the environment
+ self.info(bold('pickling environment... '), nonl=True)
+ self.env.topickle(path.join(self.doctreedir, ENV_PICKLE_FILENAME))
+ self.info('done')
+
+ # global actions
+ self.info(bold('checking consistency... '), nonl=True)
+ self.env.check_consistency()
+ self.info('done')
+ else:
+ if method == 'update' and not docnames:
+ self.info(bold('no targets are out of date.'))
+ return
+
+ # another indirection to support builders that don't build
+ # files individually
+ self.write(docnames, list(updated_docnames), method)
+
+ # finish (write static files etc.)
+ self.finish()
+ status = (self.app.statuscode == 0 and 'succeeded'
+ or 'finished with problems')
+ if self.app._warncount:
+ self.info(bold('build %s, %s warning%s.' %
+ (status, self.app._warncount,
+ self.app._warncount != 1 and 's' or '')))
+ else:
+ self.info(bold('build %s.' % status))
+
+ def write(self, build_docnames, updated_docnames, method='update'):
+ if build_docnames is None or build_docnames == ['__all__']:
+ # build_all
+ build_docnames = self.env.found_docs
+ if method == 'update':
+ # build updated ones as well
+ docnames = set(build_docnames) | set(updated_docnames)
+ else:
+ docnames = set(build_docnames)
+
+ # add all toctree-containing files that may have changed
+ for docname in list(docnames):
+ for tocdocname in self.env.files_to_rebuild.get(docname, []):
+ docnames.add(tocdocname)
+ docnames.add(self.config.master_doc)
+
+ self.info(bold('preparing documents... '), nonl=True)
+ self.prepare_writing(docnames)
+ self.info('done')
+
+ # write target files
+ warnings = []
+ self.env.set_warnfunc(lambda *args: warnings.append(args))
+ for docname in self.status_iterator(
+ sorted(docnames), 'writing output... ', darkgreen, len(docnames)):
+ doctree = self.env.get_and_resolve_doctree(docname, self)
+ self.write_doc(docname, doctree)
+ for warning in warnings:
+ self.warn(*warning)
+ self.env.set_warnfunc(self.warn)
+
+ def prepare_writing(self, docnames):
+ raise NotImplementedError
+
+ def write_doc(self, docname, doctree):
+ raise NotImplementedError
+
+ def finish(self):
+ """
+ Finish the building process. The default implementation does nothing.
+ """
+ pass
+
+ def cleanup(self):
+ """
+ Cleanup any resources. The default implementation does nothing.
+ """
+
+
+BUILTIN_BUILDERS = {
+ 'html': ('html', 'StandaloneHTMLBuilder'),
+ 'dirhtml': ('html', 'DirectoryHTMLBuilder'),
+ 'pickle': ('html', 'PickleHTMLBuilder'),
+ 'json': ('html', 'JSONHTMLBuilder'),
+ 'web': ('html', 'PickleHTMLBuilder'),
+ 'htmlhelp': ('htmlhelp', 'HTMLHelpBuilder'),
+ 'qthelp': ('qthelp', 'QtHelpBuilder'),
+ 'latex': ('latex', 'LaTeXBuilder'),
+ 'text': ('text', 'TextBuilder'),
+ 'changes': ('changes', 'ChangesBuilder'),
+ 'linkcheck': ('linkcheck', 'CheckExternalLinksBuilder'),
+}
diff --git a/sphinx/builders/changes.py b/sphinx/builders/changes.py
new file mode 100644
index 00000000..e07b06d8
--- /dev/null
+++ b/sphinx/builders/changes.py
@@ -0,0 +1,156 @@
+# -*- coding: utf-8 -*-
+"""
+ sphinx.builders.changes
+ ~~~~~~~~~~~~~~~~~~~~~~~
+
+ Changelog builder.
+
+ :copyright: Copyright 2007-2009 by the Sphinx team, see AUTHORS.
+ :license: BSD, see LICENSE for details.
+"""
+
+import codecs
+import shutil
+from os import path
+from cgi import escape
+
+from sphinx import package_dir
+from sphinx.util import ensuredir, os_path, copy_static_entry
+from sphinx.theming import Theme
+from sphinx.builders import Builder
+from sphinx.util.console import bold
+
+
+class ChangesBuilder(Builder):
+ """
+ Write a summary with all versionadded/changed directives.
+ """
+ name = 'changes'
+
+ def init(self):
+ self.create_template_bridge()
+ Theme.init_themes(self)
+ self.theme = Theme('default')
+ self.templates.init(self, self.theme)
+
+ def get_outdated_docs(self):
+ return self.outdir
+
+ typemap = {
+ 'versionadded': 'added',
+ 'versionchanged': 'changed',
+ 'deprecated': 'deprecated',
+ }
+
+ def write(self, *ignored):
+ version = self.config.version
+ libchanges = {}
+ apichanges = []
+ otherchanges = {}
+ if version not in self.env.versionchanges:
+ self.info(bold('no changes in version %s.' % version))
+ return
+ self.info(bold('writing summary file...'))
+ for type, docname, lineno, module, descname, content in \
+ self.env.versionchanges[version]:
+ if isinstance(descname, tuple):
+ descname = descname[0]
+ ttext = self.typemap[type]
+ context = content.replace('\n', ' ')
+ if descname and docname.startswith('c-api'):
+ if not descname:
+ continue
+ if context:
+ entry = '<b>%s</b>: <i>%s:</i> %s' % (descname, ttext,
+ context)
+ else:
+ entry = '<b>%s</b>: <i>%s</i>.' % (descname, ttext)
+ apichanges.append((entry, docname, lineno))
+ elif descname or module:
+ if not module:
+ module = _('Builtins')
+ if not descname:
+ descname = _('Module level')
+ if context:
+ entry = '<b>%s</b>: <i>%s:</i> %s' % (descname, ttext,
+ context)
+ else:
+ entry = '<b>%s</b>: <i>%s</i>.' % (descname, ttext)
+ libchanges.setdefault(module, []).append((entry, docname,
+ lineno))
+ else:
+ if not context:
+ continue
+ entry = '<i>%s:</i> %s' % (ttext.capitalize(), context)
+ title = self.env.titles[docname].astext()
+ otherchanges.setdefault((docname, title), []).append(
+ (entry, docname, lineno))
+
+ ctx = {
+ 'project': self.config.project,
+ 'version': version,
+ 'docstitle': self.config.html_title,
+ 'shorttitle': self.config.html_short_title,
+ 'libchanges': sorted(libchanges.iteritems()),
+ 'apichanges': sorted(apichanges),
+ 'otherchanges': sorted(otherchanges.iteritems()),
+ 'show_sphinx': self.config.html_show_sphinx,
+ }
+ f = codecs.open(path.join(self.outdir, 'index.html'), 'w', 'utf8')
+ try:
+ f.write(self.templates.render('changes/frameset.html', ctx))
+ finally:
+ f.close()
+ f = codecs.open(path.join(self.outdir, 'changes.html'), 'w', 'utf8')
+ try:
+ f.write(self.templates.render('changes/versionchanges.html', ctx))
+ finally:
+ f.close()
+
+ hltext = ['.. versionadded:: %s' % version,
+ '.. versionchanged:: %s' % version,
+ '.. deprecated:: %s' % version]
+
+ def hl(no, line):
+ line = '<a name="L%s"> </a>' % no + escape(line)
+ for x in hltext:
+ if x in line:
+ line = '<span class="hl">%s</span>' % line
+ break
+ return line
+
+ self.info(bold('copying source files...'))
+ for docname in self.env.all_docs:
+ f = codecs.open(self.env.doc2path(docname), 'r', 'latin1')
+ lines = f.readlines()
+ targetfn = path.join(self.outdir, 'rst', os_path(docname)) + '.html'
+ ensuredir(path.dirname(targetfn))
+ f = codecs.open(targetfn, 'w', 'latin1')
+ try:
+ text = ''.join(hl(i+1, line) for (i, line) in enumerate(lines))
+ ctx = {
+ 'filename': self.env.doc2path(docname, None),
+ 'text': text
+ }
+ f.write(self.templates.render('changes/rstsource.html', ctx))
+ finally:
+ f.close()
+ themectx = dict(('theme_' + key, val) for (key, val) in
+ self.theme.get_options({}).iteritems())
+ copy_static_entry(path.join(package_dir, 'themes', 'default',
+ 'static', 'default.css_t'),
+ path.join(self.outdir, 'default.css_t'),
+ self, themectx)
+ copy_static_entry(path.join(package_dir, 'themes', 'basic',
+ 'static', 'basic.css'),
+ path.join(self.outdir, 'basic.css'), self)
+
+ def hl(self, text, version):
+ text = escape(text)
+ for directive in ['versionchanged', 'versionadded', 'deprecated']:
+ text = text.replace('.. %s:: %s' % (directive, version),
+ '<b>.. %s:: %s</b>' % (directive, version))
+ return text
+
+ def finish(self):
+ pass
diff --git a/sphinx/builders/html.py b/sphinx/builders/html.py
new file mode 100644
index 00000000..365cf5f9
--- /dev/null
+++ b/sphinx/builders/html.py
@@ -0,0 +1,836 @@
+# -*- coding: utf-8 -*-
+"""
+ sphinx.builders.html
+ ~~~~~~~~~~~~~~~~~~~~
+
+ Several HTML builders.
+
+ :copyright: Copyright 2007-2009 by the Sphinx team, see AUTHORS.
+ :license: BSD, see LICENSE for details.
+"""
+
+import os
+import codecs
+import shutil
+import posixpath
+import cPickle as pickle
+from os import path
+try:
+ from hashlib import md5
+except ImportError:
+ # 2.4 compatibility
+ from md5 import md5
+
+from docutils import nodes
+from docutils.io import DocTreeInput, StringOutput
+from docutils.core import publish_parts
+from docutils.utils import new_document
+from docutils.frontend import OptionParser
+from docutils.readers.doctree import Reader as DoctreeReader
+
+from sphinx import package_dir, __version__
+from sphinx.util import SEP, os_path, relative_uri, ensuredir, \
+ movefile, ustrftime, copy_static_entry
+from sphinx.errors import SphinxError
+from sphinx.search import js_index
+from sphinx.theming import Theme
+from sphinx.builders import Builder, ENV_PICKLE_FILENAME
+from sphinx.highlighting import PygmentsBridge
+from sphinx.util.console import bold
+from sphinx.writers.html import HTMLWriter, HTMLTranslator, \
+ SmartyPantsHTMLTranslator
+
+try:
+ import json
+except ImportError:
+ try:
+ import simplejson as json
+ except ImportError:
+ json = None
+
+#: the filename for the inventory of objects
+INVENTORY_FILENAME = 'objects.inv'
+#: the filename for the "last build" file (for serializing builders)
+LAST_BUILD_FILENAME = 'last_build'
+
+
+class StandaloneHTMLBuilder(Builder):
+ """
+ Builds standalone HTML docs.
+ """
+ name = 'html'
+ format = 'html'
+ copysource = True
+ out_suffix = '.html'
+ link_suffix = '.html' # defaults to matching out_suffix
+ indexer_format = js_index
+ supported_image_types = ['image/svg+xml', 'image/png',
+ 'image/gif', 'image/jpeg']
+ searchindex_filename = 'searchindex.js'
+ add_permalinks = True
+ embedded = False # for things like HTML help or Qt help: suppresses sidebar
+
+ # This is a class attribute because it is mutated by Sphinx.add_javascript.
+ script_files = ['_static/jquery.js', '_static/doctools.js']
+
+ def init(self):
+ # a hash of all config values that, if changed, cause a full rebuild
+ self.config_hash = ''
+ self.tags_hash = ''
+ # section numbers for headings in the currently visited document
+ self.secnumbers = {}
+
+ self.init_templates()
+ self.init_highlighter()
+ self.init_translator_class()
+ if self.config.html_file_suffix:
+ self.out_suffix = self.config.html_file_suffix
+
+ if self.config.html_link_suffix is not None:
+ self.link_suffix = self.config.html_link_suffix
+ else:
+ self.link_suffix = self.out_suffix
+
+ if self.config.language is not None:
+ jsfile = path.join(package_dir, 'locale', self.config.language,
+ 'LC_MESSAGES', 'sphinx.js')
+ if path.isfile(jsfile):
+ self.script_files.append('_static/translations.js')
+
+ def init_templates(self):
+ Theme.init_themes(self)
+ self.theme = Theme(self.config.html_theme)
+ self.create_template_bridge()
+ self.templates.init(self, self.theme)
+
+ def init_highlighter(self):
+ # determine Pygments style and create the highlighter
+ if self.config.pygments_style is not None:
+ style = self.config.pygments_style
+ elif self.theme:
+ style = self.theme.get_confstr('theme', 'pygments_style', 'none')
+ else:
+ style = 'sphinx'
+ self.highlighter = PygmentsBridge('html', style)
+
+ def init_translator_class(self):
+ if self.config.html_translator_class:
+ self.translator_class = self.app.import_object(
+ self.config.html_translator_class,
+ 'html_translator_class setting')
+ elif self.config.html_use_smartypants:
+ self.translator_class = SmartyPantsHTMLTranslator
+ else:
+ self.translator_class = HTMLTranslator
+
+ def get_outdated_docs(self):
+ cfgdict = dict((name, self.config[name])
+ for (name, desc) in self.config.values.iteritems()
+ if desc[1] == 'html')
+ self.config_hash = md5(str(cfgdict)).hexdigest()
+ self.tags_hash = md5(str(sorted(self.tags))).hexdigest()
+ old_config_hash = old_tags_hash = ''
+ try:
+ fp = open(path.join(self.outdir, '.buildinfo'))
+ version = fp.readline()
+ if version.rstrip() != '# Sphinx build info version 1':
+ raise ValueError
+ fp.readline() # skip commentary
+ cfg, old_config_hash = fp.readline().strip().split(': ')
+ if cfg != 'config':
+ raise ValueError
+ tag, old_tags_hash = fp.readline().strip().split(': ')
+ if tag != 'tags':
+ raise ValueError
+ fp.close()
+ except ValueError:
+ self.warn('unsupported build info format in %r, building all' %
+ path.join(self.outdir, '.buildinfo'))
+ except Exception:
+ pass
+ if old_config_hash != self.config_hash or \
+ old_tags_hash != self.tags_hash:
+ for docname in self.env.found_docs:
+ yield docname
+ return
+
+ if self.templates:
+ template_mtime = self.templates.newest_template_mtime()
+ else:
+ template_mtime = 0
+ for docname in self.env.found_docs:
+ if docname not in self.env.all_docs:
+ yield docname
+ continue
+ targetname = self.env.doc2path(docname, self.outdir,
+ self.out_suffix)
+ try:
+ targetmtime = path.getmtime(targetname)
+ except Exception:
+ targetmtime = 0
+ try:
+ srcmtime = max(path.getmtime(self.env.doc2path(docname)),
+ template_mtime)
+ if srcmtime > targetmtime:
+ yield docname
+ except EnvironmentError:
+ # source doesn't exist anymore
+ pass
+
+ def render_partial(self, node):
+ """Utility: Render a lone doctree node."""
+ doc = new_document('<partial node>')
+ doc.append(node)
+ return publish_parts(
+ doc,
+ source_class=DocTreeInput,
+ reader=DoctreeReader(),
+ writer=HTMLWriter(self),
+ settings_overrides={'output_encoding': 'unicode'}
+ )
+
+ def prepare_writing(self, docnames):
+ from sphinx.search import IndexBuilder
+
+ self.indexer = IndexBuilder(self.env)
+ self.load_indexer(docnames)
+ self.docwriter = HTMLWriter(self)
+ self.docsettings = OptionParser(
+ defaults=self.env.settings,
+ components=(self.docwriter,)).get_default_values()
+
+ # format the "last updated on" string, only once is enough since it
+ # typically doesn't include the time of day
+ lufmt = self.config.html_last_updated_fmt
+ if lufmt is not None:
+ self.last_updated = ustrftime(lufmt or _('%b %d, %Y'))
+ else:
+ self.last_updated = None
+
+ logo = self.config.html_logo and \
+ path.basename(self.config.html_logo) or ''
+
+ favicon = self.config.html_favicon and \
+ path.basename(self.config.html_favicon) or ''
+ if favicon and os.path.splitext(favicon)[1] != '.ico':
+ self.warn('html_favicon is not an .ico file')
+
+ if not isinstance(self.config.html_use_opensearch, basestring):
+ self.warn('html_use_opensearch config value must now be a string')
+
+ self.relations = self.env.collect_relations()
+
+ rellinks = []
+ if self.config.html_use_index:
+ rellinks.append(('genindex', _('General Index'), 'I', _('index')))
+ if self.config.html_use_modindex and self.env.modules:
+ rellinks.append(('modindex', _('Global Module Index'),
+ 'M', _('modules')))
+
+ if self.config.html_style is not None:
+ stylename = self.config.html_style
+ elif self.theme:
+ stylename = self.theme.get_confstr('theme', 'stylesheet')
+ else:
+ stylename = 'default.css'
+
+ self.globalcontext = dict(
+ embedded = self.embedded,
+ project = self.config.project,
+ release = self.config.release,
+ version = self.config.version,
+ last_updated = self.last_updated,
+ copyright = self.config.copyright,
+ master_doc = self.config.master_doc,
+ use_opensearch = self.config.html_use_opensearch,
+ docstitle = self.config.html_title,
+ shorttitle = self.config.html_short_title,
+ show_sphinx = self.config.html_show_sphinx,
+ has_source = self.config.html_copy_source,
+ show_source = self.config.html_show_sourcelink,
+ file_suffix = self.out_suffix,
+ script_files = self.script_files,
+ sphinx_version = __version__,
+ style = stylename,
+ rellinks = rellinks,
+ builder = self.name,
+ parents = [],
+ logo = logo,
+ favicon = favicon,
+ )
+ if self.theme:
+ self.globalcontext.update(
+ ('theme_' + key, val) for (key, val) in
+ self.theme.get_options(
+ self.config.html_theme_options).iteritems())
+ self.globalcontext.update(self.config.html_context)
+
+ def get_doc_context(self, docname, body, metatags):
+ """Collect items for the template context of a page."""
+ # find out relations
+ prev = next = None
+ parents = []
+ rellinks = self.globalcontext['rellinks'][:]
+ related = self.relations.get(docname)
+ titles = self.env.titles
+ if related and related[2]:
+ try:
+ next = {
+ 'link': self.get_relative_uri(docname, related[2]),
+ 'title': self.render_partial(titles[related[2]])['title']
+ }
+ rellinks.append((related[2], next['title'], 'N', _('next')))
+ except KeyError:
+ next = None
+ if related and related[1]:
+ try:
+ prev = {
+ 'link': self.get_relative_uri(docname, related[1]),
+ 'title': self.render_partial(titles[related[1]])['title']
+ }
+ rellinks.append((related[1], prev['title'], 'P', _('previous')))
+ except KeyError:
+ # the relation is (somehow) not in the TOC tree, handle
+ # that gracefully
+ prev = None
+ while related and related[0]:
+ try:
+ parents.append(
+ {'link': self.get_relative_uri(docname, related[0]),
+ 'title': self.render_partial(titles[related[0]])['title']})
+ except KeyError:
+ pass
+ related = self.relations.get(related[0])
+ if parents:
+ parents.pop() # remove link to the master file; we have a generic
+ # "back to index" link already
+ parents.reverse()
+
+ # title rendered as HTML
+ title = titles.get(docname)
+ title = title and self.render_partial(title)['title'] or ''
+ # the name for the copied source
+ sourcename = self.config.html_copy_source and docname + '.txt' or ''
+
+ # metadata for the document
+ meta = self.env.metadata.get(docname)
+
+ # local TOC and global TOC tree
+ toc = self.render_partial(self.env.get_toc_for(docname))['fragment']
+
+ return dict(
+ parents = parents,
+ prev = prev,
+ next = next,
+ title = title,
+ meta = meta,
+ body = body,
+ metatags = metatags,
+ rellinks = rellinks,
+ sourcename = sourcename,
+ toc = toc,
+ # only display a TOC if there's more than one item to show
+ display_toc = (self.env.toc_num_entries[docname] > 1),
+ )
+
+ def write_doc(self, docname, doctree):
+ destination = StringOutput(encoding='utf-8')
+ doctree.settings = self.docsettings
+
+ self.secnumbers = self.env.toc_secnumbers.get(docname, {})
+ self.imgpath = relative_uri(self.get_target_uri(docname), '_images')
+ self.post_process_images(doctree)
+ self.dlpath = relative_uri(self.get_target_uri(docname), '_downloads')
+ self.docwriter.write(doctree, destination)
+ self.docwriter.assemble_parts()
+ body = self.docwriter.parts['fragment']
+ metatags = self.docwriter.clean_meta
+
+ ctx = self.get_doc_context(docname, body, metatags)
+ self.index_page(docname, doctree, ctx.get('title', ''))
+ self.handle_page(docname, ctx, event_arg=doctree)
+
+ def finish(self):
+ self.info(bold('writing additional files...'), nonl=1)
+
+ # the global general index
+
+ if self.config.html_use_index:
+ # the total count of lines for each index letter, used to distribute
+ # the entries into two columns
+ genindex = self.env.create_index(self)
+ indexcounts = []
+ for _, entries in genindex:
+ indexcounts.append(sum(1 + len(subitems)
+ for _, (_, subitems) in entries))
+
+ genindexcontext = dict(
+ genindexentries = genindex,
+ genindexcounts = indexcounts,
+ split_index = self.config.html_split_index,
+ )
+ self.info(' genindex', nonl=1)
+
+ if self.config.html_split_index:
+ self.handle_page('genindex', genindexcontext,
+ 'genindex-split.html')
+ self.handle_page('genindex-all', genindexcontext,
+ 'genindex.html')
+ for (key, entries), count in zip(genindex, indexcounts):
+ ctx = {'key': key, 'entries': entries, 'count': count,
+ 'genindexentries': genindex}
+ self.handle_page('genindex-' + key, ctx,
+ 'genindex-single.html')
+ else:
+ self.handle_page('genindex', genindexcontext, 'genindex.html')
+
+ # the global module index
+
+ if self.config.html_use_modindex and self.env.modules:
+ # the sorted list of all modules, for the global module index
+ modules = sorted(((mn, (self.get_relative_uri('modindex', fn) +
+ '#module-' + mn, sy, pl, dep))
+ for (mn, (fn, sy, pl, dep)) in
+ self.env.modules.iteritems()),
+ key=lambda x: x[0].lower())
+ # collect all platforms
+ platforms = set()
+ # sort out collapsable modules
+ modindexentries = []
+ letters = []
+ pmn = ''
+ num_toplevels = 0
+ num_collapsables = 0
+ cg = 0 # collapse group
+ fl = '' # first letter
+ for mn, (fn, sy, pl, dep) in modules:
+ pl = pl and pl.split(', ') or []
+ platforms.update(pl)
+
+ ignore = self.env.config['modindex_common_prefix']
+ ignore = sorted(ignore, key=len, reverse=True)
+ for i in ignore:
+ if mn.startswith(i):
+ mn = mn[len(i):]
+ stripped = i
+ break
+ else:
+ stripped = ''
+
+ if fl != mn[0].lower() and mn[0] != '_':
+ # heading
+ letter = mn[0].upper()
+ if letter not in letters:
+ modindexentries.append(['', False, 0, False,
+ letter, '', [], False, ''])
+ letters.append(letter)
+ tn = mn.split('.')[0]
+ if tn != mn:
+ # submodule
+ if pmn == tn:
+ # first submodule - make parent collapsable
+ modindexentries[-1][1] = True
+ num_collapsables += 1
+ elif not pmn.startswith(tn):
+ # submodule without parent in list, add dummy entry
+ cg += 1
+ modindexentries.append([tn, True, cg, False, '', '',
+ [], False, stripped])
+ else:
+ num_toplevels += 1
+ cg += 1
+ modindexentries.append([mn, False, cg, (tn != mn), fn, sy, pl,
+ dep, stripped])
+ pmn = mn
+ fl = mn[0].lower()
+ platforms = sorted(platforms)
+
+ # apply heuristics when to collapse modindex at page load:
+ # only collapse if number of toplevel modules is larger than
+ # number of submodules
+ collapse = len(modules) - num_toplevels < num_toplevels
+
+ # As some parts of the module names may have been stripped, those
+ # names have changed, thus it is necessary to sort the entries.
+ if ignore:
+ def sorthelper(entry):
+ name = entry[0]
+ if name == '':
+ # heading
+ name = entry[4]
+ return name.lower()
+
+ modindexentries.sort(key=sorthelper)
+ letters.sort()
+
+ modindexcontext = dict(
+ modindexentries = modindexentries,
+ platforms = platforms,
+ letters = letters,
+ collapse_modindex = collapse,
+ )
+ self.info(' modindex', nonl=1)
+ self.handle_page('modindex', modindexcontext, 'modindex.html')
+
+ # the search page
+ if self.name != 'htmlhelp':
+ self.info(' search', nonl=1)
+ self.handle_page('search', {}, 'search.html')
+
+ # additional pages from conf.py
+ for pagename, template in self.config.html_additional_pages.items():
+ self.info(' '+pagename, nonl=1)
+ self.handle_page(pagename, {}, template)
+
+ if self.config.html_use_opensearch and self.name != 'htmlhelp':
+ self.info(' opensearch', nonl=1)
+ fn = path.join(self.outdir, '_static', 'opensearch.xml')
+ self.handle_page('opensearch', {}, 'opensearch.xml', outfilename=fn)
+
+ self.info()
+
+ # copy image files
+ if self.images:
+ self.info(bold('copying images...'), nonl=True)
+ ensuredir(path.join(self.outdir, '_images'))
+ for src, dest in self.images.iteritems():
+ self.info(' '+src, nonl=1)
+ shutil.copyfile(path.join(self.srcdir, src),
+ path.join(self.outdir, '_images', dest))
+ self.info()
+
+ # copy downloadable files
+ if self.env.dlfiles:
+ self.info(bold('copying downloadable files...'), nonl=True)
+ ensuredir(path.join(self.outdir, '_downloads'))
+ for src, (_, dest) in self.env.dlfiles.iteritems():
+ self.info(' '+src, nonl=1)
+ shutil.copyfile(path.join(self.srcdir, src),
+ path.join(self.outdir, '_downloads', dest))
+ self.info()
+
+ # copy static files
+ self.info(bold('copying static files... '), nonl=True)
+ ensuredir(path.join(self.outdir, '_static'))
+ # first, create pygments style file
+ f = open(path.join(self.outdir, '_static', 'pygments.css'), 'w')
+ f.write(self.highlighter.get_stylesheet())
+ f.close()
+ # then, copy translations JavaScript file
+ if self.config.language is not None:
+ jsfile = path.join(package_dir, 'locale', self.config.language,
+ 'LC_MESSAGES', 'sphinx.js')
+ if path.isfile(jsfile):
+ shutil.copyfile(jsfile, path.join(self.outdir, '_static',
+ 'translations.js'))
+ # then, copy over all user-supplied static files
+ if self.theme:
+ staticdirnames = [path.join(themepath, 'static')
+ for themepath in self.theme.get_dirchain()[::-1]]
+ else:
+ staticdirnames = []
+ staticdirnames += [path.join(self.confdir, spath)
+ for spath in self.config.html_static_path]
+ for staticdirname in staticdirnames:
+ if not path.isdir(staticdirname):
+ self.warn('static directory %r does not exist' % staticdirname)
+ continue
+ for filename in os.listdir(staticdirname):
+ if filename.startswith('.'):
+ continue
+ fullname = path.join(staticdirname, filename)
+ targetname = path.join(self.outdir, '_static', filename)
+ copy_static_entry(fullname, targetname, self,
+ self.globalcontext)
+ # last, copy logo file (handled differently)
+ if self.config.html_logo:
+ logobase = path.basename(self.config.html_logo)
+ shutil.copyfile(path.join(self.confdir, self.config.html_logo),
+ path.join(self.outdir, '_static', logobase))
+
+ # write build info file
+ fp = open(path.join(self.outdir, '.buildinfo'), 'w')
+ try:
+ fp.write('# Sphinx build info version 1\n'
+ '# This file hashes the configuration used when building'
+ ' these files. When it is not found, a full rebuild will'
+ ' be done.\nconfig: %s\ntags: %s\n' %
+ (self.config_hash, self.tags_hash))
+ finally:
+ fp.close()
+
+ self.info('done')
+
+ # dump the search index
+ self.handle_finish()
+
+ def cleanup(self):
+ # clean up theme stuff
+ if self.theme:
+ self.theme.cleanup()
+
+ def post_process_images(self, doctree):
+ """
+ Pick the best candiate for an image and link down-scaled images to
+ their high res version.
+ """
+ Builder.post_process_images(self, doctree)
+ for node in doctree.traverse(nodes.image):
+ if not node.has_key('scale') or \
+ isinstance(node.parent, nodes.reference):
+ # docutils does unfortunately not preserve the
+ # ``target`` attribute on images, so we need to check
+ # the parent node here.
+ continue
+ uri = node['uri']
+ reference = nodes.reference()
+ if uri in self.images:
+ reference['refuri'] = posixpath.join(self.imgpath,
+ self.images[uri])
+ else:
+ reference['refuri'] = uri
+ node.replace_self(reference)
+ reference.append(node)
+
+ def load_indexer(self, docnames):
+ keep = set(self.env.all_docs) - set(docnames)
+ try:
+ f = open(path.join(self.outdir, self.searchindex_filename), 'rb')
+ try:
+ self.indexer.load(f, self.indexer_format)
+ finally:
+ f.close()
+ except (IOError, OSError, ValueError):
+ if keep:
+ self.warn('search index couldn\'t be loaded, but not all '
+ 'documents will be built: the index will be '
+ 'incomplete.')
+ # delete all entries for files that will be rebuilt
+ self.indexer.prune(keep)
+
+ def index_page(self, pagename, doctree, title):
+ # only index pages with title
+ if self.indexer is not None and title:
+ self.indexer.feed(pagename, title, doctree)
+
+ def _get_local_toctree(self, docname, collapse=True):
+ return self.render_partial(self.env.get_toctree_for(
+ docname, self, collapse))['fragment']
+
+ def get_outfilename(self, pagename):
+ return path.join(self.outdir, os_path(pagename) + self.out_suffix)
+
+ # --------- these are overwritten by the serialization builder
+
+ def get_target_uri(self, docname, typ=None):
+ return docname + self.link_suffix
+
+ def handle_page(self, pagename, addctx, templatename='page.html',
+ outfilename=None, event_arg=None):
+ ctx = self.globalcontext.copy()
+ # current_page_name is backwards compatibility
+ ctx['pagename'] = ctx['current_page_name'] = pagename
+
+ def pathto(otheruri, resource=False,
+ baseuri=self.get_target_uri(pagename)):
+ if not resource:
+ otheruri = self.get_target_uri(otheruri)
+ return relative_uri(baseuri, otheruri)
+ ctx['pathto'] = pathto
+ ctx['hasdoc'] = lambda name: name in self.env.all_docs
+ ctx['customsidebar'] = self.config.html_sidebars.get(pagename)
+ ctx['toctree'] = lambda **kw: self._get_local_toctree(pagename, **kw)
+ ctx.update(addctx)
+
+ self.app.emit('html-page-context', pagename, templatename,
+ ctx, event_arg)
+
+ output = self.templates.render(templatename, ctx)
+ if not outfilename:
+ outfilename = self.get_outfilename(pagename)
+ # outfilename's path is in general different from self.outdir
+ ensuredir(path.dirname(outfilename))
+ try:
+ f = codecs.open(outfilename, 'w', 'utf-8')
+ try:
+ f.write(output)
+ finally:
+ f.close()
+ except (IOError, OSError), err:
+ self.warn("error writing file %s: %s" % (outfilename, err))
+ if self.copysource and ctx.get('sourcename'):
+ # copy the source file for the "show source" link
+ source_name = path.join(self.outdir, '_sources',
+ os_path(ctx['sourcename']))
+ ensuredir(path.dirname(source_name))
+ shutil.copyfile(self.env.doc2path(pagename), source_name)
+
+ def handle_finish(self):
+ self.info(bold('dumping search index... '), nonl=True)
+ self.indexer.prune(self.env.all_docs)
+ searchindexfn = path.join(self.outdir, self.searchindex_filename)
+ # first write to a temporary file, so that if dumping fails,
+ # the existing index won't be overwritten
+ f = open(searchindexfn + '.tmp', 'wb')
+ try:
+ self.indexer.dump(f, self.indexer_format)
+ finally:
+ f.close()
+ movefile(searchindexfn + '.tmp', searchindexfn)
+ self.info('done')
+
+ self.info(bold('dumping object inventory... '), nonl=True)
+ f = open(path.join(self.outdir, INVENTORY_FILENAME), 'w')
+ try:
+ f.write('# Sphinx inventory version 1\n')
+ f.write('# Project: %s\n' % self.config.project.encode('utf-8'))
+ f.write('# Version: %s\n' % self.config.version)
+ for modname, info in self.env.modules.iteritems():
+ f.write('%s mod %s\n' % (modname, self.get_target_uri(info[0])))
+ for refname, (docname, desctype) in self.env.descrefs.iteritems():
+ f.write('%s %s %s\n' % (refname, desctype,
+ self.get_target_uri(docname)))
+ finally:
+ f.close()
+ self.info('done')
+
+
+class DirectoryHTMLBuilder(StandaloneHTMLBuilder):
+ """
+ A StandaloneHTMLBuilder that creates all HTML pages as "index.html" in
+ a directory given by their pagename, so that generated URLs don't have
+ ``.html`` in them.
+ """
+ name = 'dirhtml'
+
+ def get_target_uri(self, docname, typ=None):
+ if docname == 'index':
+ return ''
+ if docname.endswith(SEP + 'index'):
+ return docname[:-5] # up to sep
+ return docname + SEP
+
+ def get_outfilename(self, pagename):
+ if pagename == 'index' or pagename.endswith(SEP + 'index'):
+ outfilename = path.join(self.outdir, os_path(pagename)
+ + self.out_suffix)
+ else:
+ outfilename = path.join(self.outdir, os_path(pagename),
+ 'index' + self.out_suffix)
+
+ return outfilename
+
+
+class SerializingHTMLBuilder(StandaloneHTMLBuilder):
+ """
+ An abstract builder that serializes the generated HTML.
+ """
+ #: the serializing implementation to use. Set this to a module that
+ #: implements a `dump`, `load`, `dumps` and `loads` functions
+ #: (pickle, simplejson etc.)
+ implementation = None
+
+ #: the filename for the global context file
+ globalcontext_filename = None
+
+ supported_image_types = ['image/svg+xml', 'image/png',
+ 'image/gif', 'image/jpeg']
+
+ def init(self):
+ self.config_hash = ''
+ self.tags_hash = ''
+ self.theme = None # no theme necessary
+ self.templates = None # no template bridge necessary
+ self.init_translator_class()
+ self.init_highlighter()
+
+ def get_target_uri(self, docname, typ=None):
+ if docname == 'index':
+ return ''
+ if docname.endswith(SEP + 'index'):
+ return docname[:-5] # up to sep
+ return docname + SEP
+
+ def handle_page(self, pagename, ctx, templatename='page.html',
+ outfilename=None, event_arg=None):
+ ctx['current_page_name'] = pagename
+ sidebarfile = self.config.html_sidebars.get(pagename)
+ if sidebarfile:
+ ctx['customsidebar'] = sidebarfile
+
+ if not outfilename:
+ outfilename = path.join(self.outdir,
+ os_path(pagename) + self.out_suffix)
+
+ self.app.emit('html-page-context', pagename, templatename,
+ ctx, event_arg)
+
+ ensuredir(path.dirname(outfilename))
+ f = open(outfilename, 'wb')
+ try:
+ self.implementation.dump(ctx, f, 2)
+ finally:
+ f.close()
+
+ # if there is a source file, copy the source file for the
+ # "show source" link
+ if ctx.get('sourcename'):
+ source_name = path.join(self.outdir, '_sources',
+ os_path(ctx['sourcename']))
+ ensuredir(path.dirname(source_name))
+ shutil.copyfile(self.env.doc2path(pagename), source_name)
+
+ def handle_finish(self):
+ # dump the global context
+ outfilename = path.join(self.outdir, self.globalcontext_filename)
+ f = open(outfilename, 'wb')
+ try:
+ self.implementation.dump(self.globalcontext, f, 2)
+ finally:
+ f.close()
+
+ # super here to dump the search index
+ StandaloneHTMLBuilder.handle_finish(self)
+
+ # copy the environment file from the doctree dir to the output dir
+ # as needed by the web app
+ shutil.copyfile(path.join(self.doctreedir, ENV_PICKLE_FILENAME),
+ path.join(self.outdir, ENV_PICKLE_FILENAME))
+
+ # touch 'last build' file, used by the web application to determine
+ # when to reload its environment and clear the cache
+ open(path.join(self.outdir, LAST_BUILD_FILENAME), 'w').close()
+
+
+class PickleHTMLBuilder(SerializingHTMLBuilder):
+ """
+ A Builder that dumps the generated HTML into pickle files.
+ """
+ implementation = pickle
+ indexer_format = pickle
+ name = 'pickle'
+ out_suffix = '.fpickle'
+ globalcontext_filename = 'globalcontext.pickle'
+ searchindex_filename = 'searchindex.pickle'
+
+# compatibility alias
+WebHTMLBuilder = PickleHTMLBuilder
+
+
+class JSONHTMLBuilder(SerializingHTMLBuilder):
+ """
+ A builder that dumps the generated HTML into JSON files.
+ """
+ implementation = json
+ indexer_format = json
+ name = 'json'
+ out_suffix = '.fjson'
+ globalcontext_filename = 'globalcontext.json'
+ searchindex_filename = 'searchindex.json'
+
+ def init(self):
+ if json is None:
+ raise SphinxError(
+ 'The module simplejson (or json in Python >= 2.6) '
+ 'is not available. The JSONHTMLBuilder builder will not work.')
+ SerializingHTMLBuilder.init(self)
diff --git a/sphinx/builders/htmlhelp.py b/sphinx/builders/htmlhelp.py
new file mode 100644
index 00000000..8d17f91b
--- /dev/null
+++ b/sphinx/builders/htmlhelp.py
@@ -0,0 +1,250 @@
+# -*- coding: utf-8 -*-
+"""
+ sphinx.builders.htmlhelp
+ ~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Build HTML help support files.
+ Parts adapted from Python's Doc/tools/prechm.py.
+
+ :copyright: Copyright 2007-2009 by the Sphinx team, see AUTHORS.
+ :license: BSD, see LICENSE for details.
+"""
+
+import os
+import cgi
+from os import path
+
+from docutils import nodes
+
+from sphinx import addnodes
+from sphinx.builders.html import StandaloneHTMLBuilder
+
+
+# Project file (*.hhp) template. 'outname' is the file basename (like
+# the pythlp in pythlp.hhp); 'version' is the doc version number (like
+# the 2.2 in Python 2.2).
+# The magical numbers in the long line under [WINDOWS] set most of the
+# user-visible features (visible buttons, tabs, etc).
+# About 0x10384e: This defines the buttons in the help viewer. The
+# following defns are taken from htmlhelp.h. Not all possibilities
+# actually work, and not all those that work are available from the Help
+# Workshop GUI. In particular, the Zoom/Font button works and is not
+# available from the GUI. The ones we're using are marked with 'x':
+#
+# 0x000002 Hide/Show x
+# 0x000004 Back x
+# 0x000008 Forward x
+# 0x000010 Stop
+# 0x000020 Refresh
+# 0x000040 Home x
+# 0x000080 Forward
+# 0x000100 Back
+# 0x000200 Notes
+# 0x000400 Contents
+# 0x000800 Locate x
+# 0x001000 Options x
+# 0x002000 Print x
+# 0x004000 Index
+# 0x008000 Search
+# 0x010000 History
+# 0x020000 Favorites
+# 0x040000 Jump 1
+# 0x080000 Jump 2
+# 0x100000 Zoom/Font x
+# 0x200000 TOC Next
+# 0x400000 TOC Prev
+
+project_template = '''\
+[OPTIONS]
+Binary TOC=Yes
+Binary Index=No
+Compiled file=%(outname)s.chm
+Contents file=%(outname)s.hhc
+Default Window=%(outname)s
+Default topic=index.html
+Display compile progress=No
+Full text search stop list file=%(outname)s.stp
+Full-text search=Yes
+Index file=%(outname)s.hhk
+Language=0x409
+Title=%(title)s
+
+[WINDOWS]
+%(outname)s="%(title)s","%(outname)s.hhc","%(outname)s.hhk",\
+"index.html","index.html",,,,,0x63520,220,0x10384e,[0,0,1024,768],,,,,,,0
+
+[FILES]
+'''
+
+contents_header = '''\
+<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
+<HTML>
+<HEAD>
+<meta name="GENERATOR" content="Microsoft&reg; HTML Help Workshop 4.1">
+<!-- Sitemap 1.0 -->
+</HEAD><BODY>
+<OBJECT type="text/site properties">
+ <param name="Window Styles" value="0x801227">
+ <param name="ImageType" value="Folder">
+</OBJECT>
+<UL>
+'''
+
+contents_footer = '''\
+</UL></BODY></HTML>
+'''
+
+object_sitemap = '''\
+<OBJECT type="text/sitemap">
+ <param name="Name" value="%s">
+ <param name="Local" value="%s">
+</OBJECT>
+'''
+
+# List of words the full text search facility shouldn't index. This
+# becomes file outname.stp. Note that this list must be pretty small!
+# Different versions of the MS docs claim the file has a maximum size of
+# 256 or 512 bytes (including \r\n at the end of each line).
+# Note that "and", "or", "not" and "near" are operators in the search
+# language, so no point indexing them even if we wanted to.
+stopwords = """
+a and are as at
+be but by
+for
+if in into is it
+near no not
+of on or
+such
+that the their then there these they this to
+was will with
+""".split()
+
+
+class HTMLHelpBuilder(StandaloneHTMLBuilder):
+ """
+ Builder that also outputs Windows HTML help project, contents and
+ index files. Adapted from the original Doc/tools/prechm.py.
+ """
+ name = 'htmlhelp'
+
+ # don't copy the reST source
+ copysource = False
+ supported_image_types = ['image/png', 'image/gif', 'image/jpeg']
+
+ # don't add links
+ add_permalinks = False
+ # don't add sidebar etc.
+ embedded = True
+
+ def init(self):
+ StandaloneHTMLBuilder.init(self)
+ # the output files for HTML help must be .html only
+ self.out_suffix = '.html'
+
+ def handle_finish(self):
+ self.build_hhx(self.outdir, self.config.htmlhelp_basename)
+
+ def build_hhx(self, outdir, outname):
+ self.info('dumping stopword list...')
+ f = open(path.join(outdir, outname+'.stp'), 'w')
+ try:
+ for word in sorted(stopwords):
+ print >>f, word
+ finally:
+ f.close()
+
+ self.info('writing project file...')
+ f = open(path.join(outdir, outname+'.hhp'), 'w')
+ try:
+ f.write(project_template % {'outname': outname,
+ 'title': self.config.html_title,
+ 'version': self.config.version,
+ 'project': self.config.project})
+ if not outdir.endswith(os.sep):
+ outdir += os.sep
+ olen = len(outdir)
+ for root, dirs, files in os.walk(outdir):
+ staticdir = (root == path.join(outdir, '_static'))
+ for fn in files:
+ if (staticdir and not fn.endswith('.js')) or \
+ fn.endswith('.html'):
+ print >>f, path.join(root, fn)[olen:].replace(os.sep,
+ '\\')
+ finally:
+ f.close()
+
+ self.info('writing TOC file...')
+ f = open(path.join(outdir, outname+'.hhc'), 'w')
+ try:
+ f.write(contents_header)
+ # special books
+ f.write('<LI> ' + object_sitemap % (self.config.html_short_title,
+ 'index.html'))
+ if self.config.html_use_modindex:
+ f.write('<LI> ' + object_sitemap % (_('Global Module Index'),
+ 'modindex.html'))
+ # the TOC
+ tocdoc = self.env.get_and_resolve_doctree(
+ self.config.master_doc, self, prune_toctrees=False)
+ def write_toc(node, ullevel=0):
+ if isinstance(node, nodes.list_item):
+ f.write('<LI> ')
+ for subnode in node:
+ write_toc(subnode, ullevel)
+ elif isinstance(node, nodes.reference):
+ link = node['refuri']
+ title = cgi.escape(node.astext()).replace('"','&quot;')
+ item = object_sitemap % (title, link)
+ f.write(item.encode('ascii', 'xmlcharrefreplace'))
+ elif isinstance(node, nodes.bullet_list):
+ if ullevel != 0:
+ f.write('<UL>\n')
+ for subnode in node:
+ write_toc(subnode, ullevel+1)
+ if ullevel != 0:
+ f.write('</UL>\n')
+ elif isinstance(node, addnodes.compact_paragraph):
+ for subnode in node:
+ write_toc(subnode, ullevel)
+ def istoctree(node):
+ return isinstance(node, addnodes.compact_paragraph) and \
+ node.has_key('toctree')
+ for node in tocdoc.traverse(istoctree):
+ write_toc(node)
+ f.write(contents_footer)
+ finally:
+ f.close()
+
+ self.info('writing index file...')
+ index = self.env.create_index(self)
+ f = open(path.join(outdir, outname+'.hhk'), 'w')
+ try:
+ f.write('<UL>\n')
+ def write_index(title, refs, subitems):
+ def write_param(name, value):
+ item = ' <param name="%s" value="%s">\n' % (name, value)
+ f.write(item.encode('ascii', 'xmlcharrefreplace'))
+ title = cgi.escape(title)
+ f.write('<LI> <OBJECT type="text/sitemap">\n')
+ write_param('Keyword', title)
+ if len(refs) == 0:
+ write_param('See Also', title)
+ elif len(refs) == 1:
+ write_param('Local', refs[0])
+ else:
+ for i, ref in enumerate(refs):
+ # XXX: better title?
+ write_param('Name', '[%d] %s' % (i, ref))
+ write_param('Local', ref)
+ f.write('</OBJECT>\n')
+ if subitems:
+ f.write('<UL> ')
+ for subitem in subitems:
+ write_index(subitem[0], subitem[1], [])
+ f.write('</UL>')
+ for (key, group) in index:
+ for title, (refs, subitems) in group:
+ write_index(title, refs, subitems)
+ f.write('</UL>\n')
+ finally:
+ f.close()
diff --git a/sphinx/builders/latex.py b/sphinx/builders/latex.py
new file mode 100644
index 00000000..bc2f1ba5
--- /dev/null
+++ b/sphinx/builders/latex.py
@@ -0,0 +1,203 @@
+# -*- coding: utf-8 -*-
+"""
+ sphinx.builders.latex
+ ~~~~~~~~~~~~~~~~~~~~~
+
+ LaTeX builder.
+
+ :copyright: Copyright 2007-2009 by the Sphinx team, see AUTHORS.
+ :license: BSD, see LICENSE for details.
+"""
+
+import os
+import shutil
+from os import path
+
+from docutils import nodes
+from docutils.io import FileOutput
+from docutils.utils import new_document
+from docutils.frontend import OptionParser
+
+from sphinx import package_dir, addnodes
+from sphinx.util import SEP, texescape
+from sphinx.builders import Builder
+from sphinx.environment import NoUri
+from sphinx.util.console import bold, darkgreen
+from sphinx.writers.latex import LaTeXWriter
+
+
+class LaTeXBuilder(Builder):
+ """
+ Builds LaTeX output to create PDF.
+ """
+ name = 'latex'
+ format = 'latex'
+ supported_image_types = ['application/pdf', 'image/png',
+ 'image/gif', 'image/jpeg']
+
+ def init(self):
+ self.docnames = []
+ self.document_data = []
+ texescape.init()
+
+ def get_outdated_docs(self):
+ return 'all documents' # for now
+
+ def get_target_uri(self, docname, typ=None):
+ if typ == 'token':
+ # token references are always inside production lists and must be
+ # replaced by \token{} in LaTeX
+ return '@token'
+ if docname not in self.docnames:
+ raise NoUri
+ else:
+ return '%' + docname
+
+ def get_relative_uri(self, from_, to, typ=None):
+ # ignore source path
+ return self.get_target_uri(to, typ)
+
+ def init_document_data(self):
+ preliminary_document_data = map(list, self.config.latex_documents)
+ if not preliminary_document_data:
+ self.warn('no "latex_documents" config value found; no documents '
+ 'will be written')
+ return
+ # assign subdirs to titles
+ self.titles = []
+ for entry in preliminary_document_data:
+ docname = entry[0]
+ if docname not in self.env.all_docs:
+ self.warn('"latex_documents" config value references unknown '
+ 'document %s' % docname)
+ continue
+ self.document_data.append(entry)
+ if docname.endswith(SEP+'index'):
+ docname = docname[:-5]
+ self.titles.append((docname, entry[2]))
+
+ def write(self, *ignored):
+ docwriter = LaTeXWriter(self)
+ docsettings = OptionParser(
+ defaults=self.env.settings,
+ components=(docwriter,)).get_default_values()
+
+ self.init_document_data()
+
+ for entry in self.document_data:
+ docname, targetname, title, author, docclass = entry[:5]
+ toctree_only = False
+ if len(entry) > 5:
+ toctree_only = entry[5]
+ destination = FileOutput(
+ destination_path=path.join(self.outdir, targetname),
+ encoding='utf-8')
+ self.info("processing " + targetname + "... ", nonl=1)
+ doctree = self.assemble_doctree(docname, toctree_only,
+ appendices=((docclass == 'manual') and
+ self.config.latex_appendices or []))
+ self.post_process_images(doctree)
+ self.info("writing... ", nonl=1)
+ doctree.settings = docsettings
+ doctree.settings.author = author
+ doctree.settings.title = title
+ doctree.settings.docname = docname
+ doctree.settings.docclass = docclass
+ docwriter.write(doctree, destination)
+ self.info("done")
+
+ def assemble_doctree(self, indexfile, toctree_only, appendices):
+ self.docnames = set([indexfile] + appendices)
+ self.info(darkgreen(indexfile) + " ", nonl=1)
+ def process_tree(docname, tree):
+ tree = tree.deepcopy()
+ for toctreenode in tree.traverse(addnodes.toctree):
+ newnodes = []
+ includefiles = map(str, toctreenode['includefiles'])
+ for includefile in includefiles:
+ try:
+ self.info(darkgreen(includefile) + " ", nonl=1)
+ subtree = process_tree(
+ includefile, self.env.get_doctree(includefile))
+ self.docnames.add(includefile)
+ except Exception:
+ self.warn('toctree contains ref to nonexisting '
+ 'file %r' % includefile,
+ self.env.doc2path(docname))
+ else:
+ sof = addnodes.start_of_file(docname=includefile)
+ sof.children = subtree.children
+ newnodes.append(sof)
+ toctreenode.parent.replace(toctreenode, newnodes)
+ return tree
+ tree = self.env.get_doctree(indexfile)
+ tree['docname'] = indexfile
+ if toctree_only:
+ # extract toctree nodes from the tree and put them in a
+ # fresh document
+ new_tree = new_document('<latex output>')
+ new_sect = nodes.section()
+ new_sect += nodes.title(u'<Set title in conf.py>',
+ u'<Set title in conf.py>')
+ new_tree += new_sect
+ for node in tree.traverse(addnodes.toctree):
+ new_sect += node
+ tree = new_tree
+ largetree = process_tree(indexfile, tree)
+ largetree['docname'] = indexfile
+ for docname in appendices:
+ appendix = self.env.get_doctree(docname)
+ appendix['docname'] = docname
+ largetree.append(appendix)
+ self.info()
+ self.info("resolving references...")
+ self.env.resolve_references(largetree, indexfile, self)
+ # resolve :ref:s to distant tex files -- we can't add a cross-reference,
+ # but append the document name
+ for pendingnode in largetree.traverse(addnodes.pending_xref):
+ docname = pendingnode['refdocname']
+ sectname = pendingnode['refsectname']
+ newnodes = [nodes.emphasis(sectname, sectname)]
+ for subdir, title in self.titles:
+ if docname.startswith(subdir):
+ newnodes.append(nodes.Text(_(' (in '), _(' (in ')))
+ newnodes.append(nodes.emphasis(title, title))
+ newnodes.append(nodes.Text(')', ')'))
+ break
+ else:
+ pass
+ pendingnode.replace_self(newnodes)
+ return largetree
+
+ def finish(self):
+ # copy image files
+ if self.images:
+ self.info(bold('copying images...'), nonl=1)
+ for src, dest in self.images.iteritems():
+ self.info(' '+src, nonl=1)
+ shutil.copyfile(path.join(self.srcdir, src),
+ path.join(self.outdir, dest))
+ self.info()
+
+ # copy additional files
+ if self.config.latex_additional_files:
+ self.info(bold('copying additional files...'), nonl=1)
+ for filename in self.config.latex_additional_files:
+ self.info(' '+filename, nonl=1)
+ shutil.copyfile(path.join(self.confdir, filename),
+ path.join(self.outdir, path.basename(filename)))
+ self.info()
+
+ # the logo is handled differently
+ if self.config.latex_logo:
+ logobase = path.basename(self.config.latex_logo)
+ shutil.copyfile(path.join(self.confdir, self.config.latex_logo),
+ path.join(self.outdir, logobase))
+
+ self.info(bold('copying TeX support files... '), nonl=True)
+ staticdirname = path.join(package_dir, 'texinputs')
+ for filename in os.listdir(staticdirname):
+ if not filename.startswith('.'):
+ shutil.copyfile(path.join(staticdirname, filename),
+ path.join(self.outdir, filename))
+ self.info('done')
diff --git a/sphinx/builders/linkcheck.py b/sphinx/builders/linkcheck.py
new file mode 100644
index 00000000..f3962965
--- /dev/null
+++ b/sphinx/builders/linkcheck.py
@@ -0,0 +1,133 @@
+# -*- coding: utf-8 -*-
+"""
+ sphinx.builders.linkcheck
+ ~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ The CheckExternalLinksBuilder class.
+
+ :copyright: Copyright 2007-2009 by the Sphinx team, see AUTHORS.
+ :license: BSD, see LICENSE for details.
+"""
+
+import socket
+from os import path
+from urllib2 import build_opener, HTTPError
+
+from docutils import nodes
+
+from sphinx.builders import Builder
+from sphinx.util.console import purple, red, darkgreen
+
+# create an opener that will simulate a browser user-agent
+opener = build_opener()
+opener.addheaders = [('User-agent', 'Mozilla/5.0')]
+
+
+class CheckExternalLinksBuilder(Builder):
+ """
+ Checks for broken external links.
+ """
+ name = 'linkcheck'
+
+ def init(self):
+ self.good = set()
+ self.broken = {}
+ self.redirected = {}
+ # set a timeout for non-responding servers
+ socket.setdefaulttimeout(5.0)
+ # create output file
+ open(path.join(self.outdir, 'output.txt'), 'w').close()
+
+ def get_target_uri(self, docname, typ=None):
+ return ''
+
+ def get_outdated_docs(self):
+ return self.env.found_docs
+
+ def prepare_writing(self, docnames):
+ return
+
+ def write_doc(self, docname, doctree):
+ self.info()
+ for node in doctree.traverse(nodes.reference):
+ try:
+ self.check(node, docname)
+ except KeyError:
+ continue
+
+ def check(self, node, docname):
+ uri = node['refuri']
+
+ if '#' in uri:
+ uri = uri.split('#')[0]
+
+ if uri in self.good:
+ return
+
+ lineno = None
+ while lineno is None and node:
+ node = node.parent
+ lineno = node.line
+
+ if uri[0:5] == 'http:' or uri[0:6] == 'https:':
+ self.info(uri, nonl=1)
+
+ if uri in self.broken:
+ (r, s) = self.broken[uri]
+ elif uri in self.redirected:
+ (r, s) = self.redirected[uri]
+ else:
+ (r, s) = self.resolve(uri)
+
+ if r == 0:
+ self.info(' - ' + darkgreen('working'))
+ self.good.add(uri)
+ elif r == 2:
+ self.info(' - ' + red('broken: ') + s)
+ self.write_entry('broken', docname, lineno, uri + ': ' + s)
+ self.broken[uri] = (r, s)
+ if self.app.quiet:
+ self.warn('broken link: %s' % uri,
+ '%s:%s' % (self.env.doc2path(docname), lineno))
+ else:
+ self.info(' - ' + purple('redirected') + ' to ' + s)
+ self.write_entry('redirected', docname,
+ lineno, uri + ' to ' + s)
+ self.redirected[uri] = (r, s)
+ elif len(uri) == 0 or uri[0:7] == 'mailto:' or uri[0:4] == 'ftp:':
+ return
+ else:
+ self.warn(uri + ' - ' + red('malformed!'))
+ self.write_entry('malformed', docname, lineno, uri)
+ if self.app.quiet:
+ self.warn('malformed link: %s' % uri,
+ '%s:%s' % (self.env.doc2path(docname), lineno))
+ self.app.statuscode = 1
+
+ if self.broken:
+ self.app.statuscode = 1
+
+ def write_entry(self, what, docname, line, uri):
+ output = open(path.join(self.outdir, 'output.txt'), 'a')
+ output.write("%s:%s: [%s] %s\n" % (self.env.doc2path(docname, None),
+ line, what, uri))
+ output.close()
+
+ def resolve(self, uri):
+ try:
+ f = opener.open(uri)
+ f.close()
+ except HTTPError, err:
+ #if err.code == 403 and uri.startswith('http://en.wikipedia.org/'):
+ # # Wikipedia blocks requests from urllib User-Agent
+ # return (0, 0)
+ return (2, str(err))
+ except Exception, err:
+ return (2, str(err))
+ if f.url.rstrip('/') == uri.rstrip('/'):
+ return (0, 0)
+ else:
+ return (1, f.url)
+
+ def finish(self):
+ return
diff --git a/sphinx/builders/qthelp.py b/sphinx/builders/qthelp.py
new file mode 100644
index 00000000..7c2af1ef
--- /dev/null
+++ b/sphinx/builders/qthelp.py
@@ -0,0 +1,263 @@
+# -*- coding: utf-8 -*-
+"""
+ sphinx.builders.qthelp
+ ~~~~~~~~~~~~~~~~~~~~~~
+
+ Build input files for the Qt collection generator.
+
+ :copyright: Copyright 2007-2009 by the Sphinx team, see AUTHORS.
+ :license: BSD, see LICENSE for details.
+"""
+
+import os
+import re
+import cgi
+from os import path
+
+from docutils import nodes
+
+from sphinx import addnodes
+from sphinx.builders.html import StandaloneHTMLBuilder
+
+_idpattern = re.compile(
+ r'(?P<title>.+) (\((?P<id>[\w\.]+)( (?P<descr>\w+))?\))$')
+
+
+# Qt Help Collection Project (.qhcp).
+# Is the input file for the help collection generator.
+# It contains references to compressed help files which should be
+# included in the collection.
+# It may contain various other information for customizing Qt Assistant.
+collection_template = '''\
+<?xml version="1.0" encoding="utf-8" ?>
+<QHelpCollectionProject version="1.0">
+ <docFiles>
+ <generate>
+ <file>
+ <input>%(outname)s.qhp</input>
+ <output>%(outname)s.qch</output>
+ </file>
+ </generate>
+ <register>
+ <file>%(outname)s.qch</file>
+ </register>
+ </docFiles>
+</QHelpCollectionProject>
+'''
+
+# Qt Help Project (.qhp)
+# This is the input file for the help generator.
+# It contains the table of contents, indices and references to the
+# actual documentation files (*.html).
+# In addition it defines a unique namespace for the documentation.
+project_template = '''\
+<?xml version="1.0" encoding="UTF-8"?>
+<QtHelpProject version="1.0">
+ <namespace>%(outname)s.org.%(outname)s.%(nversion)s</namespace>
+ <virtualFolder>doc</virtualFolder>
+ <customFilter name="%(project)s %(version)s">
+ <filterAttribute>%(outname)s</filterAttribute>
+ <filterAttribute>%(version)s</filterAttribute>
+ </customFilter>
+ <filterSection>
+ <filterAttribute>%(outname)s</filterAttribute>
+ <filterAttribute>%(version)s</filterAttribute>
+ <toc>
+ <section title="%(title)s" ref="%(masterdoc)s.html">
+%(sections)s
+ </section>
+ </toc>
+ <keywords>
+%(keywords)s
+ </keywords>
+ <files>
+%(files)s
+ </files>
+ </filterSection>
+</QtHelpProject>
+'''
+
+section_template = '<section title="%(title)s" ref="%(ref)s"/>'
+file_template = ' '*12 + '<file>%(filename)s</file>'
+
+
+class QtHelpBuilder(StandaloneHTMLBuilder):
+ """
+ Builder that also outputs Qt help project, contents and index files.
+ """
+ name = 'qthelp'
+
+ # don't copy the reST source
+ copysource = False
+ supported_image_types = ['image/svg+xml', 'image/png', 'image/gif',
+ 'image/jpeg']
+
+ # don't add links
+ add_permalinks = False
+ # don't add sidebar etc.
+ embedded = True
+
+ def init(self):
+ StandaloneHTMLBuilder.init(self)
+ # the output files for HTML help must be .html only
+ self.out_suffix = '.html'
+ #self.config.html_style = 'traditional.css'
+
+ def handle_finish(self):
+ self.build_qhcp(self.outdir, self.config.qthelp_basename)
+ self.build_qhp(self.outdir, self.config.qthelp_basename)
+
+ def build_qhcp(self, outdir, outname):
+ self.info('writing collection project file...')
+ f = open(path.join(outdir, outname+'.qhcp'), 'w')
+ try:
+ f.write(collection_template % {'outname': outname})
+ finally:
+ f.close()
+
+ def build_qhp(self, outdir, outname):
+ self.info('writing project file...')
+
+ # sections
+ tocdoc = self.env.get_and_resolve_doctree(self.config.master_doc, self,
+ prune_toctrees=False)
+ istoctree = lambda node: (
+ isinstance(node, addnodes.compact_paragraph)
+ and node.has_key('toctree'))
+ sections = []
+ for node in tocdoc.traverse(istoctree):
+ sections.extend(self.write_toc(node))
+
+ if self.config.html_use_modindex:
+ item = section_template % {'title': _('Global Module Index'),
+ 'ref': 'modindex.html'}
+ sections.append(' '*4*4 + item)
+ sections = '\n'.join(sections)
+
+ # keywords
+ keywords = []
+ index = self.env.create_index(self)
+ for (key, group) in index:
+ for title, (refs, subitems) in group:
+ keywords.extend(self.build_keywords(title, refs, subitems))
+ keywords = '\n'.join(keywords)
+
+ # files
+ if not outdir.endswith(os.sep):
+ outdir += os.sep
+ olen = len(outdir)
+ projectfiles = []
+ for root, dirs, files in os.walk(outdir):
+ staticdir = (root == path.join(outdir, '_static'))
+ for fn in files:
+ if (staticdir and not fn.endswith('.js')) or \
+ fn.endswith('.html'):
+ filename = path.join(root, fn)[olen:]
+ #filename = filename.replace(os.sep, '\\') # XXX
+ projectfiles.append(file_template % {'filename': filename})
+ projectfiles = '\n'.join(projectfiles)
+
+ # write the project file
+ f = open(path.join(outdir, outname+'.qhp'), 'w')
+ try:
+ nversion = self.config.version.replace('.', '_')
+ nversion = nversion.replace(' ', '_')
+ f.write(project_template % {'outname': outname,
+ 'title': self.config.html_title,
+ 'version': self.config.version,
+ 'project': self.config.project,
+ 'nversion': nversion,
+ 'masterdoc': self.config.master_doc,
+ 'sections': sections,
+ 'keywords': keywords,
+ 'files': projectfiles})
+ finally:
+ f.close()
+
+ def isdocnode(self, node):
+ if not isinstance(node, nodes.list_item):
+ return False
+ if len(node.children) != 2:
+ return False
+ if not isinstance(node.children[0], addnodes.compact_paragraph):
+ return False
+ if not isinstance(node.children[0][0], nodes.reference):
+ return False
+ if not isinstance(node.children[1], nodes.bullet_list):
+ return False
+ return True
+
+ def write_toc(self, node, indentlevel=4):
+ parts = []
+ if self.isdocnode(node):
+ refnode = node.children[0][0]
+ link = refnode['refuri']
+ title = cgi.escape(refnode.astext()).replace('"','&quot;')
+ item = '<section title="%(title)s" ref="%(ref)s">' % {
+ 'title': title,
+ 'ref': link}
+ parts.append(' '*4*indentlevel + item)
+ for subnode in node.children[1]:
+ parts.extend(self.write_toc(subnode, indentlevel+1))
+ parts.append(' '*4*indentlevel + '</section>')
+ elif isinstance(node, nodes.list_item):
+ for subnode in node:
+ parts.extend(self.write_toc(subnode, indentlevel))
+ elif isinstance(node, nodes.reference):
+ link = node['refuri']
+ title = cgi.escape(node.astext()).replace('"','&quot;')
+ item = section_template % {'title': title, 'ref': link}
+ item = ' '*4*indentlevel + item.encode('ascii', 'xmlcharrefreplace')
+ parts.append(item.encode('ascii', 'xmlcharrefreplace'))
+ elif isinstance(node, nodes.bullet_list):
+ for subnode in node:
+ parts.extend(self.write_toc(subnode, indentlevel))
+ elif isinstance(node, addnodes.compact_paragraph):
+ for subnode in node:
+ parts.extend(self.write_toc(subnode, indentlevel))
+
+ return parts
+
+ def keyword_item(self, name, ref):
+ matchobj = _idpattern.match(name)
+ if matchobj:
+ groupdict = matchobj.groupdict()
+ shortname = groupdict['title']
+ id = groupdict.get('id')
+# descr = groupdict.get('descr')
+ if shortname.endswith('()'):
+ shortname = shortname[:-2]
+ id = '%s.%s' % (id, shortname)
+ else:
+ id = descr = None
+
+ if id:
+ item = ' '*12 + '<keyword name="%s" id="%s" ref="%s"/>' % (
+ name, id, ref)
+ else:
+ item = ' '*12 + '<keyword name="%s" ref="%s"/>' % (name, ref)
+ item.encode('ascii', 'xmlcharrefreplace')
+ return item
+
+ def build_keywords(self, title, refs, subitems):
+ keywords = []
+
+ title = cgi.escape(title)
+# if len(refs) == 0: # XXX
+# write_param('See Also', title)
+ if len(refs) == 1:
+ keywords.append(self.keyword_item(title, refs[0]))
+ elif len(refs) > 1:
+ for i, ref in enumerate(refs): # XXX
+# item = (' '*12 +
+# '<keyword name="%s [%d]" ref="%s"/>' % (
+# title, i, ref))
+# item.encode('ascii', 'xmlcharrefreplace')
+# keywords.append(item)
+ keywords.append(self.keyword_item(title, ref))
+
+ if subitems:
+ for subitem in subitems:
+ keywords.extend(self.build_keywords(subitem[0], subitem[1], []))
+
+ return keywords
diff --git a/sphinx/builders/text.py b/sphinx/builders/text.py
new file mode 100644
index 00000000..8651778c
--- /dev/null
+++ b/sphinx/builders/text.py
@@ -0,0 +1,70 @@
+# -*- coding: utf-8 -*-
+"""
+ sphinx.builders.text
+ ~~~~~~~~~~~~~~~~~~~~
+
+ Plain-text Sphinx builder.
+
+ :copyright: Copyright 2007-2009 by the Sphinx team, see AUTHORS.
+ :license: BSD, see LICENSE for details.
+"""
+
+import codecs
+from os import path
+
+from docutils.io import StringOutput
+
+from sphinx.util import ensuredir, os_path
+from sphinx.builders import Builder
+from sphinx.writers.text import TextWriter
+
+
+class TextBuilder(Builder):
+ name = 'text'
+ format = 'text'
+ out_suffix = '.txt'
+
+ def init(self):
+ pass
+
+ def get_outdated_docs(self):
+ for docname in self.env.found_docs:
+ if docname not in self.env.all_docs:
+ yield docname
+ continue
+ targetname = self.env.doc2path(docname, self.outdir,
+ self.out_suffix)
+ try:
+ targetmtime = path.getmtime(targetname)
+ except Exception:
+ targetmtime = 0
+ try:
+ srcmtime = path.getmtime(self.env.doc2path(docname))
+ if srcmtime > targetmtime:
+ yield docname
+ except EnvironmentError:
+ # source doesn't exist anymore
+ pass
+
+ def get_target_uri(self, docname, typ=None):
+ return ''
+
+ def prepare_writing(self, docnames):
+ self.writer = TextWriter(self)
+
+ def write_doc(self, docname, doctree):
+ destination = StringOutput(encoding='utf-8')
+ self.writer.write(doctree, destination)
+ outfilename = path.join(self.outdir, os_path(docname) + self.out_suffix)
+ ensuredir(path.dirname(outfilename))
+ try:
+ f = codecs.open(outfilename, 'w', 'utf-8')
+ try:
+ f.write(self.writer.output)
+ finally:
+ f.close()
+ except (IOError, OSError), err:
+ self.warn("error writing file %s: %s" % (outfilename, err))
+
+ def finish(self):
+ pass