Georg Brandl avatar Georg Brandl committed 8cafbc0

Close #4: Added a ``:download:`` role that marks a non-document file
for inclusion into the HTML output and links to it.

Comments (0)

Files changed (13)

     links to another document without the need of creating a
     label to which a ``:ref:`` could link to.
 
+  - #4: Added a ``:download:`` role that marks a non-document file
+    for inclusion into the HTML output and links to it.
+
   - The ``toctree`` directive now supports a ``:hidden:`` flag,
     which will prevent links from being generated in place of
     the directive -- this allows you to define your document

doc/markup/inline.rst

 .. note::
 
    The default role (```content```) has no special meaning by default.  You are
-   free to use it for anything you like. 
+   free to use it for anything you like.
 
 
 .. _xref-syntax:
    </people>```), the link caption will be the title of the given document.
 
 
+Referencing downloadable files
+------------------------------
+
+.. versionadded:: 0.6
+
+.. role:: download
+
+   This role lets you link to files within your source tree that are not reST
+   documents that can be viewed, but files that can be downloaded.
+
+   When you use this role, the referenced file is automatically marked for
+   inclusion in the output when building (obviously, for HTML output only).
+   All downloadable files are put into the ``_downloads`` subdirectory of the
+   output directory; duplicate filenames are handled.
+
+   An example::
+
+      See :download:`this example script <../example.py>`.
+
+   The given filename is relative to the directory the current source file is
+   contained in.  The ``../example.py`` file will be copied to the output
+   directory, and a suitable link generated to it.
+
+
 Other semantic markup
 ---------------------
 
    curly braces to indicate a "variable" part, as in ``:file:``.
 
    If you don't need the "variable part" indication, use the standard
-   ````code```` instead.   
+   ````code```` instead.
 
 
 The following roles generate external links:
 Note that there are no special roles for including hyperlinks as you can use
 the standard reST markup for that purpose.
 
+
 .. _default-substitutions:
 
 Substitutions

sphinx/addnodes.py

 # compact paragraph -- never makes a <p>
 class compact_paragraph(nodes.paragraph): pass
 
+# reference to a file to download
+class download_reference(nodes.reference): pass
+
 # for the ACKS list
 class acks(nodes.Element): pass
 
 # make them known to docutils. this is needed, because the HTML writer
 # will choke at some point if these are not added
 nodes._add_node_class_names("""index desc desc_content desc_signature
-      desc_type desc_returns
-      desc_addname desc_name desc_parameterlist desc_parameter desc_optional
+      desc_type desc_returns desc_addname desc_name desc_parameterlist
+      desc_parameter desc_optional download_reference
       centered versionmodified seealso productionlist production toctree
       pending_xref compact_paragraph highlightlang literal_emphasis
       glossary acks module start_of_file tabular_col_spec meta""".split())

sphinx/builders/html.py

         doctree.settings = self.docsettings
 
         self.imgpath = relative_uri(self.get_target_uri(docname), '_images')
+        self.dlpath = relative_uri(self.get_target_uri(docname), '_downloads')
         self.docwriter.write(doctree, destination)
         self.docwriter.assemble_parts()
         body = self.docwriter.parts['fragment']
                                 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'))

sphinx/environment.py

 from docutils.transforms.parts import ContentsFilter
 
 from sphinx import addnodes
-from sphinx.util import get_matching_docs, SEP, ustrftime, docname_join
+from sphinx.util import get_matching_docs, SEP, ustrftime, docname_join, \
+     FilenameUniqDict
 from sphinx.directives import additional_xref_types
 
 default_settings = {
 
 # This is increased every time an environment attribute is added
 # or changed to properly invalidate pickle files.
-ENV_VERSION = 26
+ENV_VERSION = 27
 
 
 default_substitutions = set([
                                     # (type, string, target, aliasname)
         self.versionchanges = {}    # version -> list of
                                     # (type, docname, lineno, module, descname, content)
-        self.images = {}            # absolute path -> (docnames, unique filename)
+        self.images = FilenameUniqDict()  # absolute path -> (docnames, unique filename)
+        self.dlfiles = FilenameUniqDict() # absolute path -> (docnames, unique filename)
 
         # These are set while parsing a file
         self.docname = None         # current document name
             self.filemodules.pop(docname, None)
             self.indexentries.pop(docname, None)
             self.glob_toctrees.discard(docname)
+            self.images.purge_doc(docname)
+            self.dlfiles.purge_doc(docname)
 
             for subfn, fnset in self.files_to_rebuild.items():
                 fnset.discard(docname)
             for version, changes in self.versionchanges.items():
                 new = [change for change in changes if change[1] != docname]
                 changes[:] = new
-            for fullpath, (docs, _) in self.images.items():
-                docs.discard(docname)
-                if not docs:
-                    del self.images[fullpath]
 
     def doc2path(self, docname, base=True, suffix=None):
         """
                       self.doc2path(config.master_doc))
 
         self.app = None
-
-        # 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]
-
         if app:
             app.emit('env-updated', self)
 
         self.filter_messages(doctree)
         self.process_dependencies(docname, doctree)
         self.process_images(docname, doctree)
+        self.process_downloads(docname, doctree)
         self.process_metadata(docname, doctree)
         self.create_title_from(docname, doctree)
         self.note_labels_from(docname, doctree)
             dep = path.join(docdir, dep)
             self.dependencies.setdefault(docname, set()).add(dep)
 
+    def process_downloads(self, docname, doctree):
+        """
+        Process downloadable file paths.
+        """
+        docdir = path.dirname(self.doc2path(docname, base=None))
+        for node in doctree.traverse(addnodes.download_reference):
+            filepath = path.normpath(path.join(docdir, node['reftarget']))
+            self.dependencies.setdefault(docname, set()).add(filepath)
+            if not os.access(path.join(self.srcdir, filepath), os.R_OK):
+                self.warn(docname, 'Download file not readable: %s' % filepath,
+                          getattr(node, 'line', None))
+                continue
+            uniquename = self.dlfiles.add_file(docname, filepath)
+            node['filename'] = uniquename
+
     def process_images(self, docname, doctree):
         """
         Process and rewrite image URIs.
         """
-        existing_names = set(v[1] for v in self.images.itervalues())
         docdir = path.dirname(self.doc2path(docname, base=None))
         for node in doctree.traverse(nodes.image):
             # Map the mimetype to the corresponding image.  The writer may
                 if not os.access(path.join(self.srcdir, imgpath), os.R_OK):
                     self.warn(docname, 'Image file not readable: %s' % imgpath,
                               node.line)
-                if imgpath in self.images:
-                    self.images[imgpath][0].add(docname)
                     continue
-                uniquename = path.basename(imgpath)
-                base, ext = path.splitext(uniquename)
-                i = 0
-                while uniquename in existing_names:
-                    i += 1
-                    uniquename = '%s%s%s' % (base, i, ext)
-                self.images[imgpath] = (set([docname]), uniquename)
-                existing_names.add(uniquename)
+                self.images.add_file(docname, imgpath)
 
     def process_metadata(self, docname, doctree):
         """
     'term': nodes.emphasis,
     'token': nodes.strong,
     'envvar': nodes.strong,
+    'download': nodes.strong,
     'option': addnodes.literal_emphasis,
 }
 
         return [innernodetypes.get(typ, nodes.literal)(
             rawtext, text, classes=['xref'])], []
     # we want a cross-reference, create the reference node
-    pnode = addnodes.pending_xref(rawtext, reftype=typ, refcaption=False,
-                                  modname=env.currmodule, classname=env.currclass)
+    nodeclass = (typ == 'download') and addnodes.download_reference or \
+                addnodes.pending_xref
+    pnode = nodeclass(rawtext, reftype=typ, refcaption=False,
+                      modname=env.currmodule, classname=env.currclass)
     # we may need the line number for warnings
     pnode.line = lineno
     # the link title may differ from the target, but by default they are the same
     'term': xfileref_role,
     'option': xfileref_role,
     'doc': xfileref_role,
+    'download': xfileref_role,
 
     'menuselection': menusel_role,
     'file': emph_literal_role,

sphinx/util/__init__.py

 def ustrftime(format, *args):
     # strftime for unicode strings
     return time.strftime(unicode(format).encode('utf-8'), *args).decode('utf-8')
+
+
+class FilenameUniqDict(dict):
+    """
+    A dictionary that automatically generates unique names for its keys,
+    interpreted as filenames, and keeps track of a set of docnames they
+    appear in.  Used for images and downloadable files in the environment.
+    """
+    def __init__(self):
+        self._existing = set()
+
+    def add_file(self, docname, newfile):
+        if newfile in self:
+            self[newfile][0].add(docname)
+            return
+        uniquename = path.basename(newfile)
+        base, ext = path.splitext(uniquename)
+        i = 0
+        while uniquename in self._existing:
+            i += 1
+            uniquename = '%s%s%s' % (base, i, ext)
+        self[newfile] = (set([docname]), uniquename)
+        self._existing.add(uniquename)
+        return uniquename
+
+    def purge_doc(self, docname):
+        for filename, (docs, _) in self.items():
+            docs.discard(docname)
+            if not docs:
+                del self[filename]
+
+    def __getstate__(self):
+        return self._existing
+
+    def __setstate__(self, state):
+        self._existing = state

sphinx/writers/html.py

     def depart_highlightlang(self, node):
         pass
 
+    def visit_download_reference(self, node):
+        if node.hasattr('filename'):
+            self.body.append('<a href="%s">' % posixpath.join(
+                self.builder.dlpath, node['filename']))
+            self.context.append('</a>')
+        else:
+            self.context.append('')
+    def depart_download_reference(self, node):
+        self.body.append(self.context.pop())
+
     # overwritten
     def visit_image(self, node):
         olduri = node['uri']

sphinx/writers/latex.py

     def depart_reference(self, node):
         self.body.append(self.context.pop())
 
+    def visit_download_reference(self, node):
+        pass
+    def depart_download_reference(self, node):
+        pass
+
     def visit_pending_xref(self, node):
         pass
     def depart_pending_xref(self, node):

sphinx/writers/text.py

     def depart_reference(self, node):
         pass
 
+    def visit_download_reference(self, node):
+        pass
+    def depart_download_reference(self, node):
+        pass
+
     def visit_emphasis(self, node):
         self.add_text('*')
     def depart_emphasis(self, node):

tests/root/includes.txt

    :encoding: latin-1
 .. include:: wrongenc.inc
    :encoding: latin-1
+
+
+Testing downloadable files
+==========================
+
+Download :download:`img.png` here.
+Download :download:`this <subdir/img.png>` there.
+Don't download :download:`this <nonexisting.png>`.

tests/test_build.py

 WARNING: %(root)s/images.txt:23: Nonlocal image URI found: http://www.python.org/logo.png
 WARNING: %(root)s/includes.txt:: (WARNING/2) Encoding 'utf-8' used for reading included \
 file u'wrongenc.inc' seems to be wrong, try giving an :encoding: option
+WARNING: %(root)s/includes.txt:34: Download file not readable: nonexisting.png
 """
 
 HTML_WARNINGS = ENV_WARNINGS + """\
     'includes.html': {
         ".//pre/span[@class='s']": u'üöä',
         ".//pre": u'Max Strauß',
+        ".//a[@href='_downloads/img.png']": '',
+        ".//a[@href='_downloads/img1.png']": '',
     },
     'autodoc.html': {
         ".//dt[@id='test_autodoc.Class']": '',

tests/test_env.py

     (root / 'new.txt').write_text('New file\n========\n')
     it = env.update(app.config, app.srcdir, app.doctreedir, app)
     msg = it.next()
-    assert '1 added, 1 changed, 1 removed' in msg
+    assert '1 added, 2 changed, 1 removed' in msg
     docnames = set()
     for docname in it:
         docnames.add(docname)
-    assert docnames == set(['contents', 'new'])
+    # "includes" is in there because it contains a reference to a nonexisting
+    # downloadable file, which is given another chance to exist
+    assert docnames == set(['contents', 'new', 'includes'])
     assert 'images' not in env.all_docs
     assert 'images' not in env.found_docs
 
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.