diff options
Diffstat (limited to 'doc/source/ext/sphinxext-archive.py')
-rw-r--r-- | doc/source/ext/sphinxext-archive.py | 184 |
1 files changed, 184 insertions, 0 deletions
diff --git a/doc/source/ext/sphinxext-archive.py b/doc/source/ext/sphinxext-archive.py new file mode 100644 index 0000000..dc1c2c8 --- /dev/null +++ b/doc/source/ext/sphinxext-archive.py @@ -0,0 +1,184 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2017 European Synchrotron Radiation Facility +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# ###########################################################################*/ +"""Sphinx extension creating a link to an archive. + +This Sphinx extension adds the possibility to create a zip archive and +include it in the generated HTML documentation. + +This extension provides an *archive* directive. + +Usage: + +.. archive:: <relative_path/to/directory/to/compress> + :filename: <name_of_the_archive_file, default: last_folder_of_directory.zip> + :filter: <Space-separated list of included file patterns, default *.*> + :basedir: <Name of the base directory in the archive, default: filename> + +Examples: + +To create a example.zip archive of the ../../example/ folder: + +.. archive:: ../../examples/ + +To get more control on the name of the archive and its content: + +.. archive:: ../../examples/ + :filename: myproject_examples.zip + :filter: *.py *.png + :basedir: myproject_examples + +WARNING: The content of this directory is not checked for outdated documents. +""" +__authors__ = ["T. Vincent"] +__license__ = "MIT" +__date__ = "01/09/2017" + + +import fnmatch +import logging +import os +import os.path +import posixpath +import shutil +import tempfile + +from docutils.parsers.rst import directives, Directive +import docutils.nodes + + +_logger = logging.getLogger(__name__) + + +# docutils directive + +class ArchiveDirective(Directive): + """Add a link to download an archive + + This directive add an :class:`archive` node to the document tree. + """ + + has_content = False + required_arguments = 1 + optional_arguments = 0 + final_argument_whitespace = False + option_spec = { + 'filename': directives.unchanged, + 'filter': directives.unchanged, + 'basedir': directives.unchanged, + } + + def run(self): + node = archive('') + + # Get rst source file containing this directive + source_file = self.state_machine.get_source_and_line()[0] + if source_file is None: + raise RuntimeError('Cannot get rst source file path') + + # Build input path from rst source file and directive argument + input_path = self.arguments[0] + if not input_path.startswith('/'): # Argument is a relative path + input_path = os.path.abspath( + os.path.join(os.path.dirname(source_file), input_path)) + node['input_path'] = input_path + + default_basedir = os.path.basename(input_path) + node['basedir'] = self.options.get('basedir', default_basedir) + node['filename'] = self.options.get('filename', + '.'.join((default_basedir, 'zip'))) + + node['filter'] = self.options.get('filter', '*.*') + + return [node] + + +# archive doctuils node + +class archive(docutils.nodes.General, docutils.nodes.Element, docutils.nodes.Inline): + """archive node created by :class:`ArchiveDirective`""" + pass + + +def visit_archive_html(self, node): + """Node visitor to translate :class:`archive` nodes to HTML. + + :param self: Sphinx HTML Writter + :param node: The :class:`archive` node to translate to HTML + :raise: SkipNode as depart is not implemented + """ + filename = node['filename'] + input_path = node['input_path'] + + # Create a temporary folder to create archive content + tmp_dir = tempfile.mkdtemp() + + # Copy selected content to temporary folder + base_dir = os.path.join(tmp_dir, node['basedir']) + + def ignore(src, names): + patterns = node['filter'].split() + ignored_names = [] + for name in names: + for pattern in patterns: + if fnmatch.fnmatch(name, pattern): + break + else: + ignored_names.append(name) + return ignored_names + + shutil.copytree(input_path, base_dir, ignore=ignore) + + # Compress temporary folder to zip + output_filename = os.path.join( + self.builder.outdir, '_downloads', filename) + root, ext = os.path.splitext(output_filename) + assert ext == '.zip' + shutil.make_archive(root, 'zip', tmp_dir, node['basedir']) + + # Clean-up temporary folder + shutil.rmtree(tmp_dir) + + # Generate HTML + relative_path = posixpath.join(self.builder.dlpath, filename) + self.body.append('<a href="%s">%s</a>' % (relative_path, filename)) + raise docutils.nodes.SkipNode + + +def visit_skip(self, node): + """No-op node visitor""" + raise docutils.nodes.SkipNode + + +# Extension setup + +def setup(app): + """Sphinx extension registration""" + app.add_node(archive, + html=(visit_archive_html, None), + latex=(visit_skip, None)) + + app.add_directive('archive', ArchiveDirective) + + return {'version': '0.1'} |