mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
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:
parent
e2c52d6e90
commit
b3d55c3139
3
CHANGES
3
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
|
||||
|
@ -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
|
||||
|
@ -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())
|
||||
|
@ -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'))
|
||||
|
@ -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):
|
||||
"""
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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']
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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>`.
|
||||
|
@ -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']": '',
|
||||
|
@ -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
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user