Close #4: Added a `:download:` role that marks a non-document file

for inclusion into the HTML output and links to it.
This commit is contained in:
Georg Brandl 2008-12-28 21:30:25 +01:00
parent e2c52d6e90
commit b3d55c3139
13 changed files with 147 additions and 32 deletions

View File

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

View File

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

View File

@ -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())

View File

@ -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'))

View File

@ -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):
"""

View File

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

View File

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

View File

@ -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']

View File

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

View File

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

View File

@ -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>`.

View File

@ -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']": '',

View File

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