# -*- coding: utf-8 -*-
"""
sphinx.builder
~~~~~~~~~~~~~~
Builder classes for different output formats.
:copyright: 2007-2008 by Georg Brandl.
:license: BSD.
"""
from __future__ import with_statement
import os
import sys
import time
import types
import codecs
import shutil
import cPickle as pickle
import cStringIO as StringIO
from os import path
from cgi import escape
from docutils.io import StringOutput, FileOutput, DocTreeInput
from docutils.core import publish_parts
from docutils.utils import new_document
from docutils.readers import doctree
from docutils.frontend import OptionParser
from .util import (get_matching_files, attrdict, status_iterator, ensuredir,
get_category, relative_uri, os_path, SEP)
from .htmlhelp import build_hhx
from .patchlevel import get_version_info, get_sys_version_info
from .htmlwriter import HTMLWriter
from .latexwriter import LaTeXWriter
from .environment import BuildEnvironment, NoUri
from .highlighting import pygments, highlight_block, get_stylesheet
from .util.console import bold, purple, green
from . import addnodes
# side effect: registers roles and directives
from . import roles
from . import directives
ENV_PICKLE_FILENAME = 'environment.pickle'
LAST_BUILD_FILENAME = 'last_build'
# Helper objects
class relpath_to(object):
def __init__(self, builder, filename):
self.baseuri = builder.get_target_uri(filename)
self.builder = builder
def __call__(self, otheruri, resource=False):
if not resource:
otheruri = self.builder.get_target_uri(otheruri)
return relative_uri(self.baseuri, otheruri)
class collect_env_warnings(object):
def __init__(self, builder):
self.builder = builder
self.warnings = []
def __enter__(self):
self.builder.env.set_warnfunc(self.warnings.append)
def __exit__(self, *args):
self.builder.env.set_warnfunc(self.builder.warn)
for warning in self.warnings:
self.builder.warn(warning)
class Builder(object):
"""
Builds target formats from the reST sources.
"""
option_spec = {}
def __init__(self, srcdirname, outdirname, doctreedirname,
options, confoverrides=None, env=None,
status_stream=None, warning_stream=None,
freshenv=False):
self.srcdir = srcdirname
self.outdir = outdirname
self.doctreedir = doctreedirname
if not path.isdir(doctreedirname):
os.mkdir(doctreedirname)
self.freshenv = freshenv
self.options = attrdict(options)
self.validate_options()
self.status_stream = status_stream or sys.stdout
self.warning_stream = warning_stream or sys.stderr
# probably set in load_env()
self.env = env
self.config = {}
execfile(path.join(srcdirname, 'conf.py'), self.config)
# remove potentially pickling-problematic values
del self.config['__builtins__']
for key, val in self.config.items():
if isinstance(val, types.ModuleType):
del self.config[key]
if confoverrides:
self.config.update(confoverrides)
# replace version info if 'auto'
if self.config['version'] == 'auto' or self.config['release'] == 'auto':
try:
version, release = get_version_info(srcdirname)
except (IOError, OSError):
version, release = get_sys_version_info()
self.warn('Can\'t get version info from Include/patchlevel.h, '
'using version of this interpreter (%s).' % release)
if self.config['version'] == 'auto':
self.config['version'] = version
if self.config['release'] == 'auto':
self.config['release'] = release
self.init()
# helper methods
def validate_options(self):
for option in self.options:
if option not in self.option_spec:
raise ValueError('Got unexpected option %s' % option)
for option in self.option_spec:
if option not in self.options:
self.options[option] = False
def msg(self, message='', nonl=False, nobold=False):
if not nobold: message = bold(message)
if nonl:
print >>self.status_stream, message,
else:
print >>self.status_stream, message
self.status_stream.flush()
def warn(self, message):
print >>self.warning_stream, 'WARNING:', message
def init(self):
"""Load necessary templates and perform initialization."""
raise NotImplementedError
def get_target_uri(self, source_filename, typ=None):
"""Return the target URI for a source filename."""
raise NotImplementedError
def get_relative_uri(self, from_, to, typ=None):
"""Return a relative URI between two source filenames."""
return relative_uri(self.get_target_uri(from_),
self.get_target_uri(to, typ))
def get_outdated_files(self):
"""Return a list of output files that are outdated."""
raise NotImplementedError
# build methods
def load_env(self):
"""Set up the build environment. Return True if a pickled file could be
successfully loaded, False if a new environment had to be created."""
if self.env:
return
if not self.freshenv:
try:
self.msg('trying to load pickled env...', nonl=True)
self.env = BuildEnvironment.frompickle(
path.join(self.doctreedir, ENV_PICKLE_FILENAME))
self.msg('done', nobold=True)
except Exception, err:
self.msg('failed: %s' % err, nobold=True)
self.env = BuildEnvironment(self.srcdir, self.doctreedir)
else:
self.env = BuildEnvironment(self.srcdir, self.doctreedir)
def build_all(self):
"""Build all source files."""
self.load_env()
self.build(None, summary='all source files')
def build_specific(self, source_filenames):
"""Only rebuild as much as needed for changes in the source_filenames."""
# bring the filenames to the canonical format, that is,
# relative to the source directory.
dirlen = len(self.srcdir) + 1
to_write = [path.abspath(filename)[dirlen:] for filename in source_filenames]
self.load_env()
self.build(to_write,
summary='%d source files given on command line' % len(to_write))
def build_update(self):
"""Only rebuild files changed or added since last build."""
self.load_env()
to_build = self.get_outdated_files()
if not to_build:
self.msg('no target files are out of date, exiting.')
return
if isinstance(to_build, str):
self.build([], 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, filenames, summary=None):
if summary:
self.msg('building [%s]:' % self.name, nonl=1)
self.msg(summary, nobold=1)
updated_filenames = []
# while reading, collect all warnings from docutils
with collect_env_warnings(self):
self.msg('reading, updating environment:', nonl=1)
iterator = self.env.update(self.config)
self.msg(iterator.next(), nonl=1, nobold=1)
for filename in iterator:
if not updated_filenames:
self.msg('')
updated_filenames.append(filename)
self.msg(purple(filename), nonl=1, nobold=1)
self.msg()
if updated_filenames:
# save the environment
self.msg('pickling the env...', nonl=True)
self.env.topickle(path.join(self.doctreedir, ENV_PICKLE_FILENAME))
self.msg('done', nobold=True)
# global actions
self.msg('checking consistency...')
self.env.check_consistency()
# another indirection to support methods which don't build files
# individually
self.write(filenames, updated_filenames)
# finish (write style files etc.)
self.msg('finishing...')
self.finish()
self.msg('done!')
def write(self, build_filenames, updated_filenames):
if build_filenames is None: # build_all
build_filenames = self.env.all_files
filenames = set(build_filenames) | set(updated_filenames)
# add all toctree-containing files that may have changed
for filename in list(filenames):
for tocfilename in self.env.files_to_rebuild.get(filename, []):
filenames.add(tocfilename)
filenames.add('contents.rst')
self.msg('creating index...')
self.env.create_index(self)
self.prepare_writing(filenames)
# write target files
with collect_env_warnings(self):
self.msg('writing output...')
for filename in status_iterator(sorted(filenames), green,
stream=self.status_stream):
doctree = self.env.get_and_resolve_doctree(filename, self)
self.write_file(filename, doctree)
def prepare_writing(self, filenames):
raise NotImplementedError
def write_file(self, filename, doctree):
raise NotImplementedError
def finish(self):
raise NotImplementedError
class StandaloneHTMLBuilder(Builder):
"""
Builds standalone HTML docs.
"""
name = 'html'
copysource = True
def init(self):
"""Load templates."""
# lazily import this, maybe other builders won't need it
from ._jinja import Environment, FileSystemLoader
# load templates
self.templates = {}
templates_path = path.join(path.dirname(__file__), 'templates')
jinja_env = Environment(loader=FileSystemLoader(templates_path),
# disable traceback, more likely that something in the
# application is broken than in the templates
friendly_traceback=False)
for fname in os.listdir(templates_path):
if fname.endswith('.html'):
self.templates[fname[:-5]] = jinja_env.get_template(fname)
def render_partial(self, node):
"""Utility: Render a lone doctree node."""
doc = new_document('foo')
doc.append(node)
return publish_parts(
doc,
source_class=DocTreeInput,
reader=doctree.Reader(),
writer=HTMLWriter(self),
settings_overrides={'output_encoding': 'unicode'}
)
def prepare_writing(self, filenames):
from .search import IndexBuilder
self.indexer = IndexBuilder()
self.load_indexer(filenames)
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.get('html_last_updated_fmt')
if lufmt:
self.last_updated = time.strftime(lufmt)
else:
self.last_updated = None
self.globalcontext = dict(
last_updated = self.last_updated,
builder = self.name,
release = self.config['release'],
version = self.config['version'],
parents = [],
len = len,
titles = {},
)
def write_file(self, filename, doctree):
destination = StringOutput(encoding='utf-8')
doctree.settings = self.docsettings
output = self.docwriter.write(doctree, destination)
self.docwriter.assemble_parts()
prev = next = None
parents = []
related = self.env.toctree_relations.get(filename)
if related:
prev = {'link': self.get_relative_uri(filename, related[1]),
'title': self.render_partial(self.env.titles[related[1]])['title']}
next = {'link': self.get_relative_uri(filename, related[2]),
'title': self.render_partial(self.env.titles[related[2]])['title']}
while related:
parents.append(
{'link': self.get_relative_uri(filename, related[0]),
'title': self.render_partial(self.env.titles[related[0]])['title']})
related = self.env.toctree_relations.get(related[0])
if parents:
parents.pop() # remove link to "contents.rst"; we have a generic
# "back to index" link already
parents.reverse()
title = self.env.titles.get(filename)
if title:
title = self.render_partial(title)['title']
else:
title = ''
self.globalcontext['titles'][filename] = title
sourcename = filename[:-4] + '.txt'
context = dict(
title = title,
sourcename = sourcename,
pathto = relpath_to(self, self.get_target_uri(filename)),
body = self.docwriter.parts['fragment'],
toc = self.render_partial(self.env.get_toc_for(filename))['fragment'],
# only display a TOC if there's more than one item to show
display_toc = (self.env.toc_num_entries[filename] > 1),
parents = parents,
prev = prev,
next = next,
)
self.index_file(filename, doctree, title)
self.handle_file(filename, context)
def finish(self):
self.msg('writing additional files...')
# the global general index
# the total count of lines for each index letter, used to distribute
# the entries into two columns
indexcounts = []
for key, entries in self.env.index:
indexcounts.append(sum(1 + len(subitems) for _, (_, subitems) in entries))
genindexcontext = dict(
genindexentries = self.env.index,
genindexcounts = indexcounts,
current_page_name = 'genindex',
pathto = relpath_to(self, self.get_target_uri('genindex.rst')),
)
self.handle_file('genindex.rst', genindexcontext, 'genindex')
# the global module index
# the sorted list of all modules, for the global module index
modules = sorted(((mn, (self.get_relative_uri('modindex.rst', 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 = []
pmn = ''
cg = 0 # collapse group
fl = '' # first letter
for mn, (fn, sy, pl, dep) in modules:
pl = pl.split(', ') if pl else []
platforms.update(pl)
if fl != mn[0].lower() and mn[0] != '_':
modindexentries.append(['', False, 0, False,
mn[0].upper(), '', [], False])
tn = mn.partition('.')[0]
if tn != mn:
# submodule
if pmn == tn:
# first submodule - make parent collapsable
modindexentries[-1][1] = True
elif not pmn.startswith(tn):
# submodule without parent in list, add dummy entry
cg += 1
modindexentries.append([tn, True, cg, False, '', '', [], False])
else:
cg += 1
modindexentries.append([mn, False, cg, (tn != mn), fn, sy, pl, dep])
pmn = mn
fl = mn[0].lower()
platforms = sorted(platforms)
modindexcontext = dict(
modindexentries = modindexentries,
platforms = platforms,
current_page_name = 'modindex',
pathto = relpath_to(self, self.get_target_uri('modindex.rst')),
)
self.handle_file('modindex.rst', modindexcontext, 'modindex')
# the download page
downloadcontext = dict(
pathto = relpath_to(self, self.get_target_uri('download.rst')),
current_page_name = 'download',
download_base_url = self.config['html_download_base_url'],
)
self.handle_file('download.rst', downloadcontext, 'download')
# the index page
indexcontext = dict(
pathto = relpath_to(self, self.get_target_uri('index.rst')),
current_page_name = 'index',
)
self.handle_file('index.rst', indexcontext, 'index')
# the search page
searchcontext = dict(
pathto = relpath_to(self, self.get_target_uri('search.rst')),
current_page_name = 'search',
)
self.handle_file('search.rst', searchcontext, 'search')
# copy style files
self.msg('copying style files...')
styledirname = path.join(path.dirname(__file__), 'style')
ensuredir(path.join(self.outdir, 'style'))
for filename in os.listdir(styledirname):
if not filename.startswith('.'):
shutil.copyfile(path.join(styledirname, filename),
path.join(self.outdir, 'style', filename))
# add pygments style file
f = open(path.join(self.outdir, 'style', 'pygments.css'), 'w')
if pygments:
f.write(get_stylesheet())
f.close()
# dump the search index
self.handle_finish()
# --------- these are overwritten by the Web builder
def get_target_uri(self, source_filename, typ=None):
return source_filename[:-4] + '.html'
def get_outdated_files(self):
for filename in get_matching_files(
self.srcdir, '*.rst', exclude=set(self.config.get('unused_files', ()))):
try:
rstname = path.join(self.outdir, os_path(filename))
targetmtime = path.getmtime(rstname[:-4] + '.html')
except:
targetmtime = 0
if filename not in self.env.all_files:
yield filename
elif path.getmtime(path.join(self.srcdir, os_path(filename))) > targetmtime:
yield filename
def load_indexer(self, filenames):
try:
with open(path.join(self.outdir, 'searchindex.json'), 'r') as f:
self.indexer.load(f, 'json')
except (IOError, OSError):
pass
# delete all entries for files that will be rebuilt
self.indexer.prune([fn[:-4] for fn in set(self.env.all_files) - set(filenames)])
def index_file(self, filename, doctree, title):
# only index pages with title
if self.indexer is not None and title:
category = get_category(filename)
if category is not None:
self.indexer.feed(self.get_target_uri(filename)[:-5], # strip '.html'
category, title, doctree)
def handle_file(self, filename, context, templatename='page'):
ctx = self.globalcontext.copy()
ctx.update(context)
output = self.templates[templatename].render(ctx)
outfilename = path.join(self.outdir, os_path(filename)[:-4] + '.html')
ensuredir(path.dirname(outfilename)) # normally different from self.outdir
try:
with codecs.open(outfilename, 'w', 'utf-8') as fp:
fp.write(output)
except (IOError, OSError), err:
self.warn("Error writing file %s: %s" % (outfilename, err))
if self.copysource and context.get('sourcename'):
# copy the source file for the "show source" link
shutil.copyfile(path.join(self.srcdir, os_path(filename)),
path.join(self.outdir, os_path(context['sourcename'])))
def handle_finish(self):
self.msg('dumping search index...')
self.indexer.prune([self.get_target_uri(fn)[:-5] for fn in self.env.all_files])
with open(path.join(self.outdir, 'searchindex.json'), 'w') as f:
self.indexer.dump(f, 'json')
class WebHTMLBuilder(StandaloneHTMLBuilder):
"""
Builds HTML docs usable with the web-based doc server.
"""
name = 'web'
def init(self):
# Nothing to do here.
pass
def get_outdated_files(self):
for filename in get_matching_files(
self.srcdir, '*.rst', exclude=set(self.config.get('unused_files', ()))):
try:
targetmtime = path.getmtime(
path.join(self.outdir, os_path(filename)[:-4] + '.fpickle'))
except:
targetmtime = 0
if path.getmtime(path.join(self.srcdir,
os_path(filename))) > targetmtime:
yield filename
def get_target_uri(self, source_filename, typ=None):
if source_filename == 'index.rst':
return ''
if source_filename.endswith(SEP+'index.rst'):
return source_filename[:-9] # up to sep
return source_filename[:-4] + SEP
def load_indexer(self, filenames):
try:
with open(path.join(self.outdir, 'searchindex.pickle'), 'r') as f:
self.indexer.load(f, 'pickle')
except (IOError, OSError):
pass
# delete all entries for files that will be rebuilt
self.indexer.prune(set(self.env.all_files) - set(filenames))
def index_file(self, filename, doctree, title):
# only index pages with title and category
if self.indexer is not None and title:
category = get_category(filename)
if category is not None:
self.indexer.feed(filename, category, title, doctree)
def handle_file(self, filename, context, templatename='page'):
outfilename = path.join(self.outdir, os_path(filename)[:-4] + '.fpickle')
ensuredir(path.dirname(outfilename))
context.pop('pathto', None) # can't be pickled
with file(outfilename, 'wb') as fp:
pickle.dump(context, fp, 2)
# if there is a source file, copy the source file for the "show source" link
if context.get('sourcename'):
source_name = path.join(self.outdir, 'sources',
os_path(context['sourcename']))
ensuredir(path.dirname(source_name))
shutil.copyfile(path.join(self.srcdir, os_path(filename)), source_name)
def handle_finish(self):
# dump the global context
outfilename = path.join(self.outdir, 'globalcontext.pickle')
with file(outfilename, 'wb') as fp:
pickle.dump(self.globalcontext, fp, 2)
self.msg('dumping search index...')
self.indexer.prune(self.env.all_files)
with open(path.join(self.outdir, 'searchindex.pickle'), 'wb') as f:
self.indexer.dump(f, 'pickle')
# 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()
# copy configuration file if not present
if not path.isfile(path.join(self.outdir, 'webconf.py')):
shutil.copyfile(path.join(path.dirname(__file__), 'web', 'webconf.py'),
path.join(self.outdir, 'webconf.py'))
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'
option_spec = {
'outname': 'Output file base name (default "pydoc")'
}
# don't copy the reST source
copysource = False
def handle_finish(self):
build_hhx(self, self.outdir, self.options.get('outname') or 'pydoc')
class LaTeXBuilder(Builder):
"""
Builds LaTeX output to create PDF.
"""
name = 'latex'
def init(self):
self.filenames = []
def get_outdated_files(self):
return 'all documents' # for now
def get_target_uri(self, source_filename, typ=None):
if typ == 'token':
# token references are always inside production lists and must be
# replaced by \token{} in LaTeX
return '@token'
if source_filename not in self.filenames:
raise NoUri
else:
return ''
def get_document_data(self):
# Python specific...
for toplevel in ["c-api", "distutils", "documenting", "extending",
"install", "reference", "tutorial", "using", "library"]:
yield (toplevel + SEP + 'index.rst', toplevel+'.tex', 'manual')
yield ('whatsnew' + SEP + self.config['version'] + '.rst',
'whatsnew.tex', 'howto')
for howto in [fn for fn in self.env.all_files
if fn.startswith('howto'+SEP)
and not fn.endswith('index.rst')]:
yield (howto, 'howto-'+howto[6:-4]+'.tex', 'howto')
def write(self, *ignored):
# first, assemble the "special" docs that are in every PDF
specials = []
for fname in ["glossary", "about", "license", "copyright"]:
specials.append(self.env.get_doctree(fname+".rst"))
docwriter = LaTeXWriter(self)
docsettings = OptionParser(
defaults=self.env.settings,
components=(docwriter,)).get_default_values()
for sourcename, targetname, docclass in self.get_document_data():
destination = FileOutput(
destination_path=path.join(self.outdir, targetname),
encoding='utf-8')
print "processing", targetname + "...",
doctree = self.assemble_doctree(
sourcename, specials=(docclass == 'manual') and specials or [])
print "writing...",
doctree.settings = docsettings
doctree.settings.filename = sourcename
doctree.settings.docclass = docclass
output = docwriter.write(doctree, destination)
print "done"
def assemble_doctree(self, indexfile, specials):
self.filenames = set([indexfile, 'glossary.rst', 'about.rst',
'license.rst', 'copyright.rst'])
print green(indexfile),
def process_tree(filename, tree):
#tree = tree.deepcopy() XXX
for toctreenode in tree.traverse(addnodes.toctree):
newnodes = []
includefiles = map(str, toctreenode['includefiles'])
for includefile in includefiles:
try:
print green(includefile),
subtree = process_tree(includefile,
self.env.get_doctree(includefile))
self.filenames.add(includefile)
except:
self.warn('%s: toctree contains ref to nonexisting file %r' %
(filename, includefile))
else:
newnodes.extend(subtree.children)
toctreenode.parent.replace(toctreenode, newnodes)
return tree
largetree = process_tree(indexfile, self.env.get_doctree(indexfile))
largetree.extend(specials)
print
print "resolving references..."
# XXX problem here: :ref:s to distant tex files
self.env.resolve_references(largetree, indexfile, self)
return largetree
def finish(self):
self.msg('copying TeX support files...')
styledirname = path.join(path.dirname(__file__), 'texinputs')
for filename in os.listdir(styledirname):
if not filename.startswith('.'):
shutil.copyfile(path.join(styledirname, filename),
path.join(self.outdir, filename))
class ChangesBuilder(Builder):
"""
Write a summary with all versionadded/changed directives.
"""
name = 'changes'
def init(self):
from ._jinja import Environment, FileSystemLoader
templates_path = path.join(path.dirname(__file__), 'templates')
jinja_env = Environment(loader=FileSystemLoader(templates_path),
# disable traceback, more likely that something in the
# application is broken than in the templates
friendly_traceback=False)
self.ftemplate = jinja_env.get_template('versionchanges_frameset.html')
self.vtemplate = jinja_env.get_template('versionchanges.html')
self.stemplate = jinja_env.get_template('rstsource.html')
def get_outdated_files(self):
return self.outdir
typemap = {
'versionadded': 'added',
'versionchanged': 'changed',
'deprecated': 'deprecated',
}
def write(self, *ignored):
ver = self.config['version']
libchanges = {}
apichanges = []
otherchanges = {}
self.msg('writing summary file...')
for type, filename, lineno, module, descname, content in \
self.env.versionchanges[ver]:
ttext = self.typemap[type]
context = content.replace('\n', ' ')
if descname and filename.startswith('c-api'):
if not descname:
continue
if context:
entry = '%s: %s: %s' % (descname, ttext, context)
else:
entry = '%s: %s.' % (descname, ttext)
apichanges.append((entry, filename, lineno))
elif descname or module:
if not module:
module = 'Builtins'
if not descname:
descname = 'Module level'
if context:
entry = '%s: %s: %s' % (descname, ttext, context)
else:
entry = '%s: %s.' % (descname, ttext)
libchanges.setdefault(module, []).append((entry, filename, lineno))
else:
if not context:
continue
entry = '%s: %s' % (ttext.capitalize(), context)
title = self.env.titles[filename].astext()
otherchanges.setdefault((filename, title), []).append(
(entry, filename, lineno))
ctx = {
'version': ver,
'libchanges': sorted(libchanges.iteritems()),
'apichanges': sorted(apichanges),
'otherchanges': sorted(otherchanges.iteritems()),
}
with open(path.join(self.outdir, 'index.html'), 'w') as f:
f.write(self.ftemplate.render(ctx))
with open(path.join(self.outdir, 'changes.html'), 'w') as f:
f.write(self.vtemplate.render(ctx))
hltext = ['.. versionadded:: %s' % ver,
'.. versionchanged:: %s' % ver,
'.. deprecated:: %s' % ver]
def hl(no, line):
line = ' ' % no + escape(line)
for x in hltext:
if x in line:
line = '%s' % line
break
return line
self.msg('copying source files...')
for filename in self.env.all_files:
with open(path.join(self.srcdir, os_path(filename))) as f:
lines = f.readlines()
targetfn = path.join(self.outdir, 'rst', os_path(filename)) + '.html'
ensuredir(path.dirname(targetfn))
with codecs.open(targetfn, 'w', 'utf8') as f:
text = ''.join(hl(i+1, line) for (i, line) in enumerate(lines))
ctx = {'filename': filename, 'text': text}
f.write(self.stemplate.render(ctx))
shutil.copyfile(path.join(path.dirname(__file__), 'style', 'default.css'),
path.join(self.outdir, 'default.css'))
def hl(self, text, ver):
text = escape(text)
for directive in ['versionchanged', 'versionadded', 'deprecated']:
text = text.replace('.. %s:: %s' % (directive, ver),
'.. %s:: %s' % (directive, ver))
return text
def finish(self):
pass
builders = {
'html': StandaloneHTMLBuilder,
'web': WebHTMLBuilder,
'htmlhelp': HTMLHelpBuilder,
'latex': LaTeXBuilder,
'changes': ChangesBuilder,
}