Support the image directive.

This commit is contained in:
Georg Brandl 2008-03-25 12:32:03 +00:00
parent 649ce723c1
commit ba47f283f6
8 changed files with 168 additions and 19 deletions

View File

@ -1,6 +1,9 @@
Changes in trunk Changes in trunk
================ ================
* sphinx.htmlwriter, sphinx.latexwriter: Support the ``.. image::``
directive by copying image files to the output directory.
* sphinx.environment: Take dependent files into account when collecting * sphinx.environment: Take dependent files into account when collecting
the set of outdated sources. the set of outdated sources.

View File

@ -205,6 +205,19 @@ The directive content follows after a blank line and is indented relative to the
directive start. directive start.
Images
------
reST supports an image directive, used like so::
.. image:: filename
(options)
When used within Sphinx, the ``filename`` given must be relative to the source
file, and Sphinx will automatically copy image files over to a subdirectory of
the output directory on building.
Footnotes Footnotes
--------- ---------

View File

@ -75,7 +75,7 @@ class Builder(object):
def init_templates(self): def init_templates(self):
"""Call if you need Jinja templates in the builder.""" """Call if you need Jinja templates in the builder."""
# lazily import this, maybe other builders won't need it # lazily import this, other builders won't need it
from sphinx._jinja import Environment, SphinxFileSystemLoader from sphinx._jinja import Environment, SphinxFileSystemLoader
# load templates # load templates
@ -103,14 +103,18 @@ class Builder(object):
raise NotImplementedError raise NotImplementedError
def get_relative_uri(self, from_, to, typ=None): 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 Return a relative URI between two source filenames. May raise environment.NoUri
sensible URI.""" if there's no way to return a sensible URI.
"""
return relative_uri(self.get_target_uri(from_), return relative_uri(self.get_target_uri(from_),
self.get_target_uri(to, typ)) self.get_target_uri(to, typ))
def get_outdated_docs(self): def get_outdated_docs(self):
"""Return a list of output files that are outdated.""" """
Return an iterable of output files that are outdated, or a string describing
what an update build will build.
"""
raise NotImplementedError raise NotImplementedError
def status_iterator(self, iterable, summary, colorfunc): def status_iterator(self, iterable, summary, colorfunc):
@ -173,7 +177,7 @@ class Builder(object):
"""Only rebuild files changed or added since last build.""" """Only rebuild files changed or added since last build."""
to_build = self.get_outdated_docs() to_build = self.get_outdated_docs()
if isinstance(to_build, str): if isinstance(to_build, str):
self.build([], to_build) self.build(['__all__'], to_build)
else: else:
to_build = list(to_build) to_build = list(to_build)
self.build(to_build, self.build(to_build,
@ -211,7 +215,7 @@ class Builder(object):
self.info(bold('checking consistency...')) self.info(bold('checking consistency...'))
self.env.check_consistency() self.env.check_consistency()
else: else:
if not docnames: if method == 'update' and not docnames:
self.info(bold('no targets are out of date.')) self.info(bold('no targets are out of date.'))
return return
@ -341,6 +345,7 @@ class StandaloneHTMLBuilder(Builder):
destination = StringOutput(encoding='utf-8') destination = StringOutput(encoding='utf-8')
doctree.settings = self.docsettings doctree.settings = self.docsettings
self.imgpath = relative_uri(self.get_target_uri(docname), '_images')
self.docwriter.write(doctree, destination) self.docwriter.write(doctree, destination)
self.docwriter.assemble_parts() self.docwriter.assemble_parts()
@ -474,8 +479,19 @@ class StandaloneHTMLBuilder(Builder):
self.info(' index', nonl=1) self.info(' index', nonl=1)
self.handle_page('index', {'indextemplate': indextemplate}, 'index.html') self.handle_page('index', {'indextemplate': indextemplate}, 'index.html')
# copy static files
self.info() self.info()
# copy image files
if self.env.images:
self.info(bold('copying images...'), nonl=1)
ensuredir(path.join(self.outdir, '_images'))
for src, dest in self.env.images.iteritems():
self.info(' '+src, nonl=1)
shutil.copyfile(path.join(self.srcdir, src),
path.join(self.outdir, '_images', dest))
self.info()
# copy static files
self.info(bold('copying static files...')) self.info(bold('copying static files...'))
ensuredir(path.join(self.outdir, 'static')) ensuredir(path.join(self.outdir, 'static'))
staticdirnames = [path.join(path.dirname(__file__), 'static')] + \ staticdirnames = [path.join(path.dirname(__file__), 'static')] + \
@ -796,6 +812,15 @@ class LaTeXBuilder(Builder):
return largetree return largetree
def finish(self): def finish(self):
# copy image files
if self.env.images:
self.info(bold('copying images...'), nonl=1)
for src, dest in self.env.images.iteritems():
self.info(' '+src, nonl=1)
shutil.copyfile(path.join(self.srcdir, src),
path.join(self.outdir, dest))
self.info()
self.info(bold('copying TeX support files...')) self.info(bold('copying TeX support files...'))
staticdirname = path.join(path.dirname(__file__), 'texinputs') staticdirname = path.join(path.dirname(__file__), 'texinputs')
for filename in os.listdir(staticdirname): for filename in os.listdir(staticdirname):

View File

@ -352,7 +352,7 @@ def desc_directive(desctype, arguments, options, content, lineno,
signode['ids'].append(fullname) signode['ids'].append(fullname)
signode['first'] = (not names) signode['first'] = (not names)
state.document.note_explicit_target(signode) state.document.note_explicit_target(signode)
env.note_descref(fullname, desctype) env.note_descref(fullname, desctype, lineno)
names.append(name) names.append(name)
env.note_index_entry('single', env.note_index_entry('single',

View File

@ -57,7 +57,7 @@ default_settings = {
# This is increased every time a new environment attribute is added # This is increased every time a new environment attribute is added
# to properly invalidate pickle files. # to properly invalidate pickle files.
ENV_VERSION = 19 ENV_VERSION = 20
def walk_depth(node, depth, maxdepth): def walk_depth(node, depth, maxdepth):
@ -251,6 +251,7 @@ class BuildEnvironment:
# (type, string, target, aliasname) # (type, string, target, aliasname)
self.versionchanges = {} # version -> list of self.versionchanges = {} # version -> list of
# (type, docname, lineno, module, descname, content) # (type, docname, lineno, module, descname, content)
self.images = {} # absolute path -> unique filename
# These are set while parsing a file # These are set while parsing a file
self.docname = None # current document name self.docname = None # current document name
@ -269,9 +270,11 @@ class BuildEnvironment:
self._warnfunc = func self._warnfunc = func
self.settings['warning_stream'] = RedirStream(func) self.settings['warning_stream'] = RedirStream(func)
def warn(self, docname, msg): def warn(self, docname, msg, lineno=None):
if docname: if docname:
self._warnfunc(self.doc2path(docname) + ':: ' + msg) if lineno is None:
lineno = ''
self._warnfunc('%s:%s: %s' % (self.doc2path(docname), lineno, msg))
else: else:
self._warnfunc('GLOBAL:: ' + msg) self._warnfunc('GLOBAL:: ' + msg)
@ -420,6 +423,12 @@ class BuildEnvironment:
self.warn(None, 'master file %s not found' % self.warn(None, 'master file %s not found' %
self.doc2path(config.master_doc)) self.doc2path(config.master_doc))
# remove all non-existing images from inventory
for imgsrc in self.images.keys():
if not os.access(path.join(self.srcdir, imgsrc), os.R_OK):
del self.images[imgsrc]
# --------- SINGLE FILE BUILDING ------------------------------------------- # --------- SINGLE FILE BUILDING -------------------------------------------
def read_doc(self, docname, src_path=None, save_parsed=True, app=None): def read_doc(self, docname, src_path=None, save_parsed=True, app=None):
@ -436,6 +445,7 @@ class BuildEnvironment:
settings_overrides=self.settings, settings_overrides=self.settings,
reader=MyStandaloneReader()) reader=MyStandaloneReader())
self.process_dependencies(docname, doctree) self.process_dependencies(docname, doctree)
self.process_images(docname, doctree)
self.process_metadata(docname, doctree) self.process_metadata(docname, doctree)
self.create_title_from(docname, doctree) self.create_title_from(docname, doctree)
self.note_labels_from(docname, doctree) self.note_labels_from(docname, doctree)
@ -482,11 +492,37 @@ class BuildEnvironment:
deps = doctree.settings.record_dependencies deps = doctree.settings.record_dependencies
if not deps: if not deps:
return return
basename = path.dirname(self.doc2path(docname, base=None)) docdir = path.dirname(self.doc2path(docname, base=None))
for dep in deps.list: for dep in deps.list:
dep = path.join(basename, dep) dep = path.join(docdir, dep)
self.dependencies.setdefault(docname, set()).add(dep) self.dependencies.setdefault(docname, set()).add(dep)
def process_images(self, docname, doctree):
"""
Process and rewrite image URIs.
"""
docdir = path.dirname(self.doc2path(docname, base=None))
for node in doctree.traverse(nodes.image):
imguri = node['uri']
if imguri.find('://') != -1:
self.warn(docname, 'Nonlocal image URI found: %s' % imguri, node.line)
else:
imgpath = path.normpath(path.join(docdir, imguri))
node['uri'] = imgpath
self.dependencies.setdefault(docname, set()).add(imgpath)
if not os.access(path.join(self.srcdir, imgpath), os.R_OK):
self.warn(docname, 'Image file not readable: %s' % imguri, node.line)
if imgpath in self.images:
continue
names = set(self.images.values())
uniquename = path.basename(imgpath)
base, ext = path.splitext(uniquename)
i = 0
while uniquename in names:
i += 1
uniquename = '%s%s%s' % (base, i, ext)
self.images[imgpath] = uniquename
def process_metadata(self, docname, doctree): def process_metadata(self, docname, doctree):
""" """
Process the docinfo part of the doctree as metadata. Process the docinfo part of the doctree as metadata.
@ -527,6 +563,8 @@ class BuildEnvironment:
if not explicit: if not explicit:
continue continue
labelid = document.nameids[name] labelid = document.nameids[name]
if labelid is None:
continue
node = document.ids[labelid] node = document.ids[labelid]
if name.isdigit() or node.has_key('refuri') or \ if name.isdigit() or node.has_key('refuri') or \
node.tagname.startswith('desc_'): node.tagname.startswith('desc_'):
@ -535,7 +573,8 @@ class BuildEnvironment:
continue continue
if name in self.labels: if name in self.labels:
self.warn(docname, 'duplicate label %s, ' % name + self.warn(docname, 'duplicate label %s, ' % name +
'other instance in %s' % self.doc2path(self.labels[name][0])) 'other instance in %s' % self.doc2path(self.labels[name][0]),
node.line)
self.anonlabels[name] = docname, labelid self.anonlabels[name] = docname, labelid
if not isinstance(node, nodes.section): if not isinstance(node, nodes.section):
# anonymous-only labels # anonymous-only labels
@ -616,11 +655,12 @@ class BuildEnvironment:
# ------- # -------
# these are called from docutils directives and therefore use self.docname # these are called from docutils directives and therefore use self.docname
# #
def note_descref(self, fullname, desctype): def note_descref(self, fullname, desctype, line):
if fullname in self.descrefs: if fullname in self.descrefs:
self.warn(self.docname, self.warn(self.docname,
'duplicate canonical description name %s, ' % fullname + 'duplicate canonical description name %s, ' % fullname +
'other instance in %s' % self.doc2path(self.descrefs[fullname][0])) 'other instance in %s' % self.doc2path(self.descrefs[fullname][0]),
line)
self.descrefs[fullname] = (self.docname, desctype) self.descrefs[fullname] = (self.docname, desctype)
def note_module(self, modname, synopsis, platform, deprecated): def note_module(self, modname, synopsis, platform, deprecated):
@ -780,7 +820,8 @@ class BuildEnvironment:
docname, labelid = self.reftargets.get((typ, target), ('', '')) docname, labelid = self.reftargets.get((typ, target), ('', ''))
if not docname: if not docname:
if typ == 'term': if typ == 'term':
self.warn(fromdocname, 'term not in glossary: %s' % target) self.warn(fromdocname, 'term not in glossary: %s' % target,
node.line)
newnode = contnode newnode = contnode
else: else:
newnode = nodes.reference('', '') newnode = nodes.reference('', '')

View File

@ -10,6 +10,7 @@
""" """
import sys import sys
from os import path
from docutils import nodes from docutils import nodes
from docutils.writers.html4css1 import Writer, HTMLTranslator as BaseTranslator from docutils.writers.html4css1 import Writer, HTMLTranslator as BaseTranslator
@ -246,6 +247,15 @@ class HTMLTranslator(BaseTranslator):
def depart_highlightlang(self, node): def depart_highlightlang(self, node):
pass pass
# overwritten
def visit_image(self, node):
olduri = node['uri']
# rewrite the URI if the environment knows about it
if olduri in self.builder.env.images:
node['uri'] = path.join(self.builder.imgpath,
self.builder.env.images[olduri])
BaseTranslator.visit_image(self, node)
def visit_toctree(self, node): def visit_toctree(self, node):
# this only happens when formatting a toc from env.tocs -- in this # this only happens when formatting a toc from env.tocs -- in this
# case we don't want to include the subtree # case we don't want to include the subtree

View File

@ -40,6 +40,15 @@ FOOTER = r'''
\end{document} \end{document}
''' '''
GRAPHICX = r'''
%% Check if we are compiling under latex or pdflatex.
\ifx\pdftexversion\undefined
\usepackage{graphicx}
\else
\usepackage[pdftex]{graphicx}
\fi
'''
class LaTeXWriter(writers.Writer): class LaTeXWriter(writers.Writer):
@ -118,11 +127,14 @@ class LaTeXTranslator(nodes.NodeVisitor):
self.first_document = 1 self.first_document = 1
self.this_is_the_title = 1 self.this_is_the_title = 1
self.literal_whitespace = 0 self.literal_whitespace = 0
self.need_graphicx = 0
def astext(self): def astext(self):
return (HEADER % self.options) + \ return (HEADER % self.options) + \
(self.options['modindex'] and '\\makemodindex\n' or '') + \ (self.options['modindex'] and '\\makemodindex\n' or '') + \
self.highlighter.get_stylesheet() + '\n\n' + \ self.highlighter.get_stylesheet() + \
(self.need_graphicx and GRAPHICX or '') + \
'\n\n' + \
u''.join(self.body) + \ u''.join(self.body) + \
(self.options['modindex'] and '\\printmodindex\n' or '') + \ (self.options['modindex'] and '\\printmodindex\n' or '') + \
(FOOTER % self.options) (FOOTER % self.options)
@ -498,6 +510,49 @@ class LaTeXTranslator(nodes.NodeVisitor):
def depart_module(self, node): def depart_module(self, node):
pass pass
def visit_image(self, node):
self.need_graphicx = 1
attrs = node.attributes
pre = [] # in reverse order
post = []
include_graphics_options = ""
inline = isinstance(node.parent, nodes.TextElement)
if attrs.has_key('scale'):
# Could also be done with ``scale`` option to
# ``\includegraphics``; doing it this way for consistency.
pre.append('\\scalebox{%f}{' % (attrs['scale'] / 100.0,))
post.append('}')
if attrs.has_key('width'):
include_graphics_options = '[width=%s]' % attrs['width']
if attrs.has_key('align'):
align_prepost = {
# By default latex aligns the top of an image.
(1, 'top'): ('', ''),
(1, 'middle'): ('\\raisebox{-0.5\\height}{', '}'),
(1, 'bottom'): ('\\raisebox{-\\height}{', '}'),
(0, 'center'): ('{\\hfill', '\\hfill}'),
# These 2 don't exactly do the right thing. The image should
# be floated alongside the paragraph. See
# http://www.w3.org/TR/html4/struct/objects.html#adef-align-IMG
(0, 'left'): ('{', '\\hfill}'),
(0, 'right'): ('{\\hfill', '}'),}
try:
pre.append(align_prepost[inline, attrs['align']][0])
post.append(align_prepost[inline, attrs['align']][1])
except KeyError:
pass
if not inline:
pre.append('\n')
post.append('\n')
pre.reverse()
self.body.extend(pre)
# XXX: for now, don't fiddle around with graphics formats
uri = self.builder.env.images.get(node['uri'], node['uri'])
self.body.append('\\includegraphics%s{%s}' % (include_graphics_options, uri))
self.body.extend(post)
def depart_image(self, node):
pass
def visit_note(self, node): def visit_note(self, node):
self.body.append('\n\\begin{notice}[note]') self.body.append('\n\\begin{notice}[note]')
def depart_note(self, node): def depart_note(self, node):

View File

@ -120,6 +120,8 @@ def xfileref_role(typ, rawtext, text, lineno, inliner, options={}, content=[]):
# we want a cross-reference, create the reference node # we want a cross-reference, create the reference node
pnode = addnodes.pending_xref(rawtext, reftype=typ, refcaption=False, pnode = addnodes.pending_xref(rawtext, reftype=typ, refcaption=False,
modname=env.currmodule, classname=env.currclass) modname=env.currmodule, classname=env.currclass)
# we may need the line number for warnings
pnode.line = lineno
innertext = text innertext = text
# special actions for Python object cross-references # special actions for Python object cross-references
if typ in ('data', 'exc', 'func', 'class', 'const', 'attr', 'meth', 'mod'): if typ in ('data', 'exc', 'func', 'class', 'const', 'attr', 'meth', 'mod'):