summaryrefslogtreecommitdiff
path: root/sphinx/builders/html.py
diff options
context:
space:
mode:
Diffstat (limited to 'sphinx/builders/html.py')
-rw-r--r--sphinx/builders/html.py836
1 files changed, 836 insertions, 0 deletions
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)