diff --git a/CHANGES b/CHANGES index e36262fc1..50b4751b2 100644 --- a/CHANGES +++ b/CHANGES @@ -21,6 +21,9 @@ New features added 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 diff --git a/doc/markup/inline.rst b/doc/markup/inline.rst index 5776c35e1..f7f998639 100644 --- a/doc/markup/inline.rst +++ b/doc/markup/inline.rst @@ -12,7 +12,7 @@ For all other roles, you have to write ``:rolename:`content```. .. 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: @@ -244,6 +244,30 @@ There is also a way to directly link to documents: ```), 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 --------------------- @@ -348,7 +372,7 @@ in a different style: 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: @@ -369,6 +393,7 @@ 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 diff --git a/sphinx/addnodes.py b/sphinx/addnodes.py index a9135b0ab..1d719d88f 100644 --- a/sphinx/addnodes.py +++ b/sphinx/addnodes.py @@ -68,6 +68,9 @@ class pending_xref(nodes.Element): pass # compact paragraph -- never makes a
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
@@ -95,8 +98,8 @@ class meta(nodes.Special, nodes.PreBibliographic, 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())
diff --git a/sphinx/builders/html.py b/sphinx/builders/html.py
index a86eaed86..86d3f0b77 100644
--- a/sphinx/builders/html.py
+++ b/sphinx/builders/html.py
@@ -226,6 +226,7 @@ class StandaloneHTMLBuilder(Builder):
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']
@@ -353,6 +354,16 @@ class StandaloneHTMLBuilder(Builder):
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'))
diff --git a/sphinx/environment.py b/sphinx/environment.py
index d709d6c2d..f77eb32e1 100644
--- a/sphinx/environment.py
+++ b/sphinx/environment.py
@@ -42,7 +42,8 @@ from docutils.transforms import Transform
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 = {
@@ -57,7 +58,7 @@ 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([
@@ -276,7 +277,8 @@ class BuildEnvironment:
# (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
@@ -317,6 +319,8 @@ class BuildEnvironment:
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)
@@ -340,10 +344,6 @@ class BuildEnvironment:
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):
"""
@@ -480,12 +480,6 @@ class BuildEnvironment:
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)
@@ -544,6 +538,7 @@ class BuildEnvironment:
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)
@@ -608,11 +603,25 @@ class BuildEnvironment:
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
@@ -656,17 +665,8 @@ class BuildEnvironment:
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):
"""
diff --git a/sphinx/roles.py b/sphinx/roles.py
index 70fca848c..d942ddc93 100644
--- a/sphinx/roles.py
+++ b/sphinx/roles.py
@@ -96,6 +96,7 @@ innernodetypes = {
'term': nodes.emphasis,
'token': nodes.strong,
'envvar': nodes.strong,
+ 'download': nodes.strong,
'option': addnodes.literal_emphasis,
}
@@ -122,8 +123,10 @@ def xfileref_role(typ, rawtext, text, lineno, inliner, options={}, content=[]):
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
@@ -236,6 +239,7 @@ specific_docroles = {
'term': xfileref_role,
'option': xfileref_role,
'doc': xfileref_role,
+ 'download': xfileref_role,
'menuselection': menusel_role,
'file': emph_literal_role,
diff --git a/sphinx/util/__init__.py b/sphinx/util/__init__.py
index 1d654134a..e25bc5a1b 100644
--- a/sphinx/util/__init__.py
+++ b/sphinx/util/__init__.py
@@ -288,3 +288,39 @@ def nested_parse_with_titles(state, content, node):
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
diff --git a/sphinx/writers/html.py b/sphinx/writers/html.py
index b9db98db8..69c1de722 100644
--- a/sphinx/writers/html.py
+++ b/sphinx/writers/html.py
@@ -259,6 +259,16 @@ class HTMLTranslator(BaseTranslator):
def depart_highlightlang(self, node):
pass
+ def visit_download_reference(self, node):
+ if node.hasattr('filename'):
+ self.body.append('' % posixpath.join(
+ self.builder.dlpath, node['filename']))
+ self.context.append('')
+ 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']
diff --git a/sphinx/writers/latex.py b/sphinx/writers/latex.py
index 226c98cfc..7695d6c5a 100644
--- a/sphinx/writers/latex.py
+++ b/sphinx/writers/latex.py
@@ -953,6 +953,11 @@ class LaTeXTranslator(nodes.NodeVisitor):
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):
diff --git a/sphinx/writers/text.py b/sphinx/writers/text.py
index cd8a464a3..f54a6386a 100644
--- a/sphinx/writers/text.py
+++ b/sphinx/writers/text.py
@@ -614,6 +614,11 @@ class TextTranslator(nodes.NodeVisitor):
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):
diff --git a/tests/root/includes.txt b/tests/root/includes.txt
index ad507fc68..d2964d3f0 100644
--- a/tests/root/includes.txt
+++ b/tests/root/includes.txt
@@ -14,3 +14,11 @@ Test file and literal inclusion
:encoding: latin-1
.. include:: wrongenc.inc
:encoding: latin-1
+
+
+Testing downloadable files
+==========================
+
+Download :download:`img.png` here.
+Download :download:`this