summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGeorg Brandl <georg@python.org>2008-12-28 21:30:25 +0100
committerGeorg Brandl <georg@python.org>2008-12-28 21:30:25 +0100
commit846f9073eef533512e102765c141c0095a913943 (patch)
treedd720b4a1b898608ee6e850c830c37c6d91c082e
parentbd5ac00a8aa0a22d0d8c9330f120748f62526afd (diff)
downloadsphinx-846f9073eef533512e102765c141c0095a913943.tar.gz
Close #4: Added a ``:download:`` role that marks a non-document file
for inclusion into the HTML output and links to it.
-rw-r--r--CHANGES3
-rw-r--r--doc/markup/inline.rst29
-rw-r--r--sphinx/addnodes.py7
-rw-r--r--sphinx/builders/html.py11
-rw-r--r--sphinx/environment.py48
-rw-r--r--sphinx/roles.py8
-rw-r--r--sphinx/util/__init__.py36
-rw-r--r--sphinx/writers/html.py10
-rw-r--r--sphinx/writers/latex.py5
-rw-r--r--sphinx/writers/text.py5
-rw-r--r--tests/root/includes.txt8
-rw-r--r--tests/test_build.py3
-rw-r--r--tests/test_env.py6
13 files changed, 147 insertions, 32 deletions
diff --git a/CHANGES b/CHANGES
index e36262fc..50b4751b 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 5776c35e..f7f99863 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:
</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
---------------------
@@ -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 a9135b0a..1d719d88 100644
--- a/sphinx/addnodes.py
+++ b/sphinx/addnodes.py
@@ -68,6 +68,9 @@ class pending_xref(nodes.Element): pass
# 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
@@ -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 a86eaed8..86d3f0b7 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 d709d6c2..f77eb32e 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 70fca848..d942ddc9 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 1d654134..e25bc5a1 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 b9db98db..69c1de72 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('<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']
diff --git a/sphinx/writers/latex.py b/sphinx/writers/latex.py
index 226c98cf..7695d6c5 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 cd8a464a..f54a6386 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 ad507fc6..d2964d3f 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 <subdir/img.png>` there.
+Don't download :download:`this <nonexisting.png>`.
diff --git a/tests/test_build.py b/tests/test_build.py
index d307818a..91506dad 100644
--- a/tests/test_build.py
+++ b/tests/test_build.py
@@ -32,6 +32,7 @@ WARNING: %(root)s/images.txt:9: Image file not readable: foo.png
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 + """\
@@ -58,6 +59,8 @@ HTML_XPATH = {
'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']": '',
diff --git a/tests/test_env.py b/tests/test_env.py
index c8a8364b..1040b88a 100644
--- a/tests/test_env.py
+++ b/tests/test_env.py
@@ -77,11 +77,13 @@ def test_second_update():
(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