diff options
Diffstat (limited to 'sphinx/builders')
| -rw-r--r-- | sphinx/builders/__init__.py | 384 | ||||
| -rw-r--r-- | sphinx/builders/changes.py | 156 | ||||
| -rw-r--r-- | sphinx/builders/html.py | 836 | ||||
| -rw-r--r-- | sphinx/builders/htmlhelp.py | 250 | ||||
| -rw-r--r-- | sphinx/builders/latex.py | 203 | ||||
| -rw-r--r-- | sphinx/builders/linkcheck.py | 133 | ||||
| -rw-r--r-- | sphinx/builders/qthelp.py | 263 | ||||
| -rw-r--r-- | sphinx/builders/text.py | 70 |
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® 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('"','"') + 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('"','"') + 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('"','"') + 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 |
