Commits

georg.brandl  committed e3ae6e2

Support the image directive.

  • Participants
  • Parent commits 73ce13a

Comments (0)

Files changed (8)

 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.
 

File doc/rest.rst

 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
 ---------
 

File sphinx/builder.py

 
     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
         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):
         """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,
             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
 
         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()
 
             self.info(' index', nonl=1)
             self.handle_page('index', {'indextemplate': indextemplate}, 'index.html')
 
+        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()
         self.info(bold('copying static files...'))
         ensuredir(path.join(self.outdir, 'static'))
         staticdirnames = [path.join(path.dirname(__file__), 'static')] + \
         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):

File sphinx/directives.py

                 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',

File sphinx/environment.py

 
 # 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):
                                     # (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
         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)
 
             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):
                                   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)
         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.
             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_'):
                 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
     # -------
     # 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):
                     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('', '')

File sphinx/htmlwriter.py

 """
 
 import sys
+from os import path
 
 from docutils import nodes
 from docutils.writers.html4css1 import Writer, HTMLTranslator as 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

File sphinx/latexwriter.py

 \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):
 
         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)
     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):

File sphinx/roles.py

     # 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'):