From ba47f283f67aad9151bbf0e4874cdff7bfad8adb Mon Sep 17 00:00:00 2001 From: Georg Brandl Date: Tue, 25 Mar 2008 12:32:03 +0000 Subject: [PATCH] Support the image directive. --- CHANGES | 3 +++ doc/rest.rst | 13 ++++++++++ sphinx/builder.py | 41 ++++++++++++++++++++++++------ sphinx/directives.py | 2 +- sphinx/environment.py | 59 ++++++++++++++++++++++++++++++++++++------- sphinx/htmlwriter.py | 10 ++++++++ sphinx/latexwriter.py | 57 ++++++++++++++++++++++++++++++++++++++++- sphinx/roles.py | 2 ++ 8 files changed, 168 insertions(+), 19 deletions(-) diff --git a/CHANGES b/CHANGES index c3cd2cab2..ba5da8d31 100644 --- a/CHANGES +++ b/CHANGES @@ -1,6 +1,9 @@ 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 the set of outdated sources. diff --git a/doc/rest.rst b/doc/rest.rst index 4984f642d..a41e84911 100644 --- a/doc/rest.rst +++ b/doc/rest.rst @@ -205,6 +205,19 @@ The directive content follows after a blank line and is indented relative to the 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 --------- diff --git a/sphinx/builder.py b/sphinx/builder.py index 47787609f..3f1ec67fa 100644 --- a/sphinx/builder.py +++ b/sphinx/builder.py @@ -75,7 +75,7 @@ class Builder(object): def init_templates(self): """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 # load templates @@ -103,14 +103,18 @@ class Builder(object): 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 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 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 def status_iterator(self, iterable, summary, colorfunc): @@ -173,7 +177,7 @@ class Builder(object): """Only rebuild files changed or added since last build.""" to_build = self.get_outdated_docs() if isinstance(to_build, str): - self.build([], to_build) + self.build(['__all__'], to_build) else: to_build = list(to_build) self.build(to_build, @@ -211,7 +215,7 @@ class Builder(object): self.info(bold('checking consistency...')) self.env.check_consistency() else: - if not docnames: + if method == 'update' and not docnames: self.info(bold('no targets are out of date.')) return @@ -341,6 +345,7 @@ class StandaloneHTMLBuilder(Builder): destination = StringOutput(encoding='utf-8') doctree.settings = self.docsettings + self.imgpath = relative_uri(self.get_target_uri(docname), '_images') self.docwriter.write(doctree, destination) self.docwriter.assemble_parts() @@ -474,8 +479,19 @@ class StandaloneHTMLBuilder(Builder): self.info(' index', nonl=1) self.handle_page('index', {'indextemplate': indextemplate}, 'index.html') - # copy static files 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...')) ensuredir(path.join(self.outdir, 'static')) staticdirnames = [path.join(path.dirname(__file__), 'static')] + \ @@ -796,6 +812,15 @@ class LaTeXBuilder(Builder): return largetree 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...')) staticdirname = path.join(path.dirname(__file__), 'texinputs') for filename in os.listdir(staticdirname): diff --git a/sphinx/directives.py b/sphinx/directives.py index 781ea2c22..50662e34c 100644 --- a/sphinx/directives.py +++ b/sphinx/directives.py @@ -352,7 +352,7 @@ def desc_directive(desctype, arguments, options, content, lineno, signode['ids'].append(fullname) signode['first'] = (not names) state.document.note_explicit_target(signode) - env.note_descref(fullname, desctype) + env.note_descref(fullname, desctype, lineno) names.append(name) env.note_index_entry('single', diff --git a/sphinx/environment.py b/sphinx/environment.py index 5f0f19a10..a7cc3964a 100644 --- a/sphinx/environment.py +++ b/sphinx/environment.py @@ -57,7 +57,7 @@ default_settings = { # This is increased every time a new environment attribute is added # to properly invalidate pickle files. -ENV_VERSION = 19 +ENV_VERSION = 20 def walk_depth(node, depth, maxdepth): @@ -251,6 +251,7 @@ class BuildEnvironment: # (type, string, target, aliasname) self.versionchanges = {} # version -> list of # (type, docname, lineno, module, descname, content) + self.images = {} # absolute path -> unique filename # These are set while parsing a file self.docname = None # current document name @@ -269,9 +270,11 @@ class BuildEnvironment: self._warnfunc = func self.settings['warning_stream'] = RedirStream(func) - def warn(self, docname, msg): + def warn(self, docname, msg, lineno=None): if docname: - self._warnfunc(self.doc2path(docname) + ':: ' + msg) + if lineno is None: + lineno = '' + self._warnfunc('%s:%s: %s' % (self.doc2path(docname), lineno, msg)) else: self._warnfunc('GLOBAL:: ' + msg) @@ -420,6 +423,12 @@ class BuildEnvironment: self.warn(None, 'master file %s not found' % 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 ------------------------------------------- def read_doc(self, docname, src_path=None, save_parsed=True, app=None): @@ -436,6 +445,7 @@ class BuildEnvironment: settings_overrides=self.settings, reader=MyStandaloneReader()) self.process_dependencies(docname, doctree) + self.process_images(docname, doctree) self.process_metadata(docname, doctree) self.create_title_from(docname, doctree) self.note_labels_from(docname, doctree) @@ -482,11 +492,37 @@ class BuildEnvironment: deps = doctree.settings.record_dependencies if not deps: return - basename = path.dirname(self.doc2path(docname, base=None)) + docdir = path.dirname(self.doc2path(docname, base=None)) for dep in deps.list: - dep = path.join(basename, dep) + dep = path.join(docdir, 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): """ Process the docinfo part of the doctree as metadata. @@ -527,6 +563,8 @@ class BuildEnvironment: if not explicit: continue labelid = document.nameids[name] + if labelid is None: + continue node = document.ids[labelid] if name.isdigit() or node.has_key('refuri') or \ node.tagname.startswith('desc_'): @@ -535,7 +573,8 @@ class BuildEnvironment: continue if name in self.labels: 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 if not isinstance(node, nodes.section): # anonymous-only labels @@ -616,11 +655,12 @@ class BuildEnvironment: # ------- # 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: self.warn(self.docname, '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) def note_module(self, modname, synopsis, platform, deprecated): @@ -780,7 +820,8 @@ class BuildEnvironment: docname, labelid = self.reftargets.get((typ, target), ('', '')) if not docname: 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 else: newnode = nodes.reference('', '') diff --git a/sphinx/htmlwriter.py b/sphinx/htmlwriter.py index 2b2c55b4a..46e36769d 100644 --- a/sphinx/htmlwriter.py +++ b/sphinx/htmlwriter.py @@ -10,6 +10,7 @@ """ import sys +from os import path from docutils import nodes from docutils.writers.html4css1 import Writer, HTMLTranslator as BaseTranslator @@ -246,6 +247,15 @@ class HTMLTranslator(BaseTranslator): def depart_highlightlang(self, node): 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): # this only happens when formatting a toc from env.tocs -- in this # case we don't want to include the subtree diff --git a/sphinx/latexwriter.py b/sphinx/latexwriter.py index 333f4159b..784112595 100644 --- a/sphinx/latexwriter.py +++ b/sphinx/latexwriter.py @@ -40,6 +40,15 @@ FOOTER = r''' \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): @@ -118,11 +127,14 @@ class LaTeXTranslator(nodes.NodeVisitor): self.first_document = 1 self.this_is_the_title = 1 self.literal_whitespace = 0 + self.need_graphicx = 0 def astext(self): return (HEADER % self.options) + \ (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) + \ (self.options['modindex'] and '\\printmodindex\n' or '') + \ (FOOTER % self.options) @@ -498,6 +510,49 @@ class LaTeXTranslator(nodes.NodeVisitor): def depart_module(self, node): 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): self.body.append('\n\\begin{notice}[note]') def depart_note(self, node): diff --git a/sphinx/roles.py b/sphinx/roles.py index 7665340d4..d9927165e 100644 --- a/sphinx/roles.py +++ b/sphinx/roles.py @@ -120,6 +120,8 @@ def xfileref_role(typ, rawtext, text, lineno, inliner, options={}, content=[]): # we want a cross-reference, create the reference node pnode = addnodes.pending_xref(rawtext, reftype=typ, refcaption=False, modname=env.currmodule, classname=env.currclass) + # we may need the line number for warnings + pnode.line = lineno innertext = text # special actions for Python object cross-references if typ in ('data', 'exc', 'func', 'class', 'const', 'attr', 'meth', 'mod'):