mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
Added an `only` directive that can selectively include text
based on enabled "tags". Tags can be given on the command line. Also, the current builder output format (e.g. "html" or "latex") is always a defined tag.
This commit is contained in:
5
CHANGES
5
CHANGES
@@ -36,6 +36,11 @@ New features added
|
||||
- #4: Added a ``:download:`` role that marks a non-document file
|
||||
for inclusion into the HTML output and links to it.
|
||||
|
||||
- Added an ``only`` directive that can selectively include text
|
||||
based on enabled "tags". Tags can be given on the command
|
||||
line. Also, the current builder output format (e.g. "html" or
|
||||
"latex") is always a defined tag.
|
||||
|
||||
- The ``literalinclude`` directive now supports several more
|
||||
options, to include only parts of a file.
|
||||
|
||||
|
||||
@@ -38,6 +38,11 @@ Important points to note:
|
||||
delete them from the namespace with ``del`` if appropriate. Modules are
|
||||
removed automatically, so you don't need to ``del`` your imports after use.
|
||||
|
||||
* There is a special object named ``tags`` available in the config file.
|
||||
It can be used to query and change the tags (see :ref:`tags`). Use
|
||||
``tags.has('tag')`` to query, ``tags.add('tag')`` and ``tags.remove('tag')``
|
||||
to change.
|
||||
|
||||
|
||||
General configuration
|
||||
---------------------
|
||||
|
||||
@@ -89,6 +89,12 @@ The :program:`sphinx-build` script has several more options:
|
||||
cross-references), but rebuild it completely. The default is to only read
|
||||
and parse source files that are new or have changed since the last run.
|
||||
|
||||
**-t** *tag*
|
||||
Define the tag *tag*. This is relevant for :dir:`only` directives that only
|
||||
include their content if this tag is set.
|
||||
|
||||
.. versionadded:: 0.6
|
||||
|
||||
**-d** *path*
|
||||
Since Sphinx has to read and parse all source files before it can write an
|
||||
output file, the parsed source files are cached as "doctree pickles".
|
||||
|
||||
@@ -48,6 +48,28 @@ Meta-information markup
|
||||
output.
|
||||
|
||||
|
||||
.. _tags:
|
||||
|
||||
Including content based on tags
|
||||
-------------------------------
|
||||
|
||||
.. directive:: .. only:: <expression>
|
||||
|
||||
Include the content of the directive only if the *expression* is true. The
|
||||
expression should consist of tags, like this::
|
||||
|
||||
.. only:: html and draft
|
||||
|
||||
Undefined tags are false, defined tags (via the ``-t`` command-line option or
|
||||
within :file:`conf.py`) are true. Boolean expressions, also using
|
||||
parentheses (like ``html and (latex or draft)`` are supported.
|
||||
|
||||
The format of the current builder (``html``, ``latex`` or ``text``) is always
|
||||
set as a tag.
|
||||
|
||||
.. versionadded:: 0.6
|
||||
|
||||
|
||||
Tables
|
||||
------
|
||||
|
||||
|
||||
2
setup.py
2
setup.py
@@ -36,7 +36,7 @@ are already present, work fine and can be seen "in action" in the Python docs:
|
||||
and inclusion of appropriately formatted docstrings.
|
||||
'''
|
||||
|
||||
requires = ['Pygments>=0.8', 'Jinja2>=2.0', 'docutils>=0.4']
|
||||
requires = ['Pygments>=0.8', 'Jinja2>=2.1', 'docutils>=0.4']
|
||||
|
||||
if sys.version_info < (2, 4):
|
||||
print 'ERROR: Sphinx requires at least Python 2.4 to run.'
|
||||
|
||||
@@ -96,6 +96,9 @@ class start_of_file(nodes.Element): pass
|
||||
# tabular column specification, used for the LaTeX writer
|
||||
class tabular_col_spec(nodes.Element): pass
|
||||
|
||||
# only (in/exclusion based on tags)
|
||||
class only(nodes.Element): pass
|
||||
|
||||
# meta directive -- same as docutils' standard meta node, but pickleable
|
||||
class meta(nodes.Special, nodes.PreBibliographic, nodes.Element): pass
|
||||
|
||||
|
||||
@@ -59,6 +59,7 @@ from sphinx.config import Config
|
||||
from sphinx.builders import BUILTIN_BUILDERS
|
||||
from sphinx.directives import GenericDesc, Target, additional_xref_types
|
||||
from sphinx.environment import SphinxStandaloneReader
|
||||
from sphinx.util.tags import Tags
|
||||
from sphinx.util.compat import Directive, directive_dwim
|
||||
from sphinx.util.console import bold
|
||||
|
||||
@@ -82,7 +83,7 @@ class Sphinx(object):
|
||||
|
||||
def __init__(self, srcdir, confdir, outdir, doctreedir, buildername,
|
||||
confoverrides, status, warning=sys.stderr, freshenv=False,
|
||||
warningiserror=False):
|
||||
warningiserror=False, tags=None):
|
||||
self.next_listener_id = 0
|
||||
self._listeners = {}
|
||||
self.builderclasses = BUILTIN_BUILDERS.copy()
|
||||
@@ -113,7 +114,8 @@ class Sphinx(object):
|
||||
self.statuscode = 0
|
||||
|
||||
# read config
|
||||
self.config = Config(confdir, CONFIG_FILENAME, confoverrides)
|
||||
self.tags = Tags(tags)
|
||||
self.config = Config(confdir, CONFIG_FILENAME, confoverrides, self.tags)
|
||||
|
||||
# load all extension modules
|
||||
for extension in self.config.extensions:
|
||||
@@ -141,6 +143,8 @@ class Sphinx(object):
|
||||
builderclass = getattr(
|
||||
__import__('sphinx.builders.' + mod, None, None, [cls]), cls)
|
||||
self.builder = builderclass(self, freshenv=freshenv)
|
||||
self.builder.tags = self.tags
|
||||
self.builder.tags.add(self.builder.format)
|
||||
self.emit('builder-inited')
|
||||
|
||||
def build(self, all_files, filenames):
|
||||
|
||||
@@ -35,6 +35,8 @@ class Builder(object):
|
||||
|
||||
# builder's name, for the -b command line options
|
||||
name = ''
|
||||
# builder's output format, or '' if no document output is produced
|
||||
format = ''
|
||||
|
||||
def __init__(self, app, env=None, freshenv=False):
|
||||
self.srcdir = app.srcdir
|
||||
|
||||
@@ -60,12 +60,13 @@ class StandaloneHTMLBuilder(Builder):
|
||||
Builds standalone HTML docs.
|
||||
"""
|
||||
name = 'html'
|
||||
format = 'html'
|
||||
copysource = True
|
||||
out_suffix = '.html'
|
||||
link_suffix = '.html' # defaults to matching out_suffix
|
||||
indexer_format = js_index
|
||||
supported_image_types = ['image/svg+xml',
|
||||
'image/png', 'image/gif', 'image/jpeg']
|
||||
supported_image_types = ['image/svg+xml', 'image/png',
|
||||
'image/gif', 'image/jpeg']
|
||||
searchindex_filename = 'searchindex.js'
|
||||
add_permalinks = True
|
||||
embedded = False # for things like HTML help or Qt help: suppresses sidebar
|
||||
@@ -76,6 +77,7 @@ class StandaloneHTMLBuilder(Builder):
|
||||
def init(self):
|
||||
# a hash of all config values that, if changed, cause a full rebuild
|
||||
self.config_hash = ''
|
||||
self.tags_hash = ''
|
||||
self.init_templates()
|
||||
self.init_highlighter()
|
||||
self.init_translator_class()
|
||||
@@ -116,23 +118,28 @@ class StandaloneHTMLBuilder(Builder):
|
||||
for (name, desc) in self.config.values.iteritems()
|
||||
if desc[1] == 'html')
|
||||
self.config_hash = md5(str(cfgdict)).hexdigest()
|
||||
self.tags_hash = md5(str(sorted(self.tags))).hexdigest()
|
||||
old_config_hash = old_tags_hash = ''
|
||||
try:
|
||||
fp = open(path.join(self.outdir, '.buildinfo'))
|
||||
version = fp.readline()
|
||||
if version.rstrip() != '# Sphinx build info version 1':
|
||||
raise ValueError
|
||||
fp.readline() # skip commentary
|
||||
cfg, old_hash = fp.readline().strip().split(': ')
|
||||
cfg, old_config_hash = fp.readline().strip().split(': ')
|
||||
if cfg != 'config':
|
||||
raise ValueError
|
||||
tag, old_tags_hash = fp.readline().strip().split(': ')
|
||||
if tag != 'tags':
|
||||
raise ValueError
|
||||
fp.close()
|
||||
except ValueError:
|
||||
self.warn('unsupported build info format in %r, building all' %
|
||||
path.join(self.outdir, '.buildinfo'))
|
||||
old_hash = ''
|
||||
except Exception:
|
||||
old_hash = ''
|
||||
if old_hash != self.config_hash:
|
||||
pass
|
||||
if old_config_hash != self.config_hash or \
|
||||
old_tags_hash != self.tags_hash:
|
||||
for docname in self.env.found_docs:
|
||||
yield docname
|
||||
return
|
||||
@@ -544,7 +551,8 @@ class StandaloneHTMLBuilder(Builder):
|
||||
fp.write('# Sphinx build info version 1\n'
|
||||
'# This file hashes the configuration used when building'
|
||||
' these files. When it is not found, a full rebuild will'
|
||||
' be done.\nconfig: %s\n' % self.config_hash)
|
||||
' be done.\nconfig: %s\ntags: %s\n' %
|
||||
(self.config_hash, self.tags_hash))
|
||||
finally:
|
||||
fp.close()
|
||||
|
||||
@@ -721,8 +729,8 @@ class SerializingHTMLBuilder(StandaloneHTMLBuilder):
|
||||
#: the filename for the global context file
|
||||
globalcontext_filename = None
|
||||
|
||||
supported_image_types = ('image/svg+xml', 'image/png', 'image/gif',
|
||||
'image/jpeg')
|
||||
supported_image_types = ['image/svg+xml', 'image/png',
|
||||
'image/gif', 'image/jpeg']
|
||||
|
||||
def init(self):
|
||||
self.init_translator_class()
|
||||
|
||||
@@ -31,8 +31,9 @@ class LaTeXBuilder(Builder):
|
||||
Builds LaTeX output to create PDF.
|
||||
"""
|
||||
name = 'latex'
|
||||
supported_image_types = ['application/pdf', 'image/png', 'image/gif',
|
||||
'image/jpeg']
|
||||
format = 'latex'
|
||||
supported_image_types = ['application/pdf', 'image/png',
|
||||
'image/gif', 'image/jpeg']
|
||||
|
||||
def init(self):
|
||||
self.docnames = []
|
||||
|
||||
@@ -21,6 +21,7 @@ from sphinx.writers.text import TextWriter
|
||||
|
||||
class TextBuilder(Builder):
|
||||
name = 'text'
|
||||
format = 'text'
|
||||
out_suffix = '.txt'
|
||||
|
||||
def init(self):
|
||||
|
||||
@@ -34,6 +34,7 @@ Options: -b <builder> -- builder to use; default is html
|
||||
-a -- write all files; default is to only write \
|
||||
new and changed files
|
||||
-E -- don't use a saved environment, always read all files
|
||||
-t <tag> -- include "only" blocks with <tag>
|
||||
-d <path> -- path for the cached environment and doctree files
|
||||
(default: outdir/.doctrees)
|
||||
-c <path> -- path where configuration file (conf.py) is located
|
||||
@@ -58,7 +59,7 @@ def main(argv):
|
||||
nocolor()
|
||||
|
||||
try:
|
||||
opts, args = getopt.getopt(argv[1:], 'ab:d:c:CD:A:NEqQWP')
|
||||
opts, args = getopt.getopt(argv[1:], 'ab:t:d:c:CD:A:NEqQWP')
|
||||
allopts = set(opt[0] for opt in opts)
|
||||
srcdir = confdir = path.abspath(args[0])
|
||||
if not path.isdir(srcdir):
|
||||
@@ -92,6 +93,7 @@ def main(argv):
|
||||
warning = sys.stderr
|
||||
confoverrides = {}
|
||||
htmlcontext = {}
|
||||
tags = []
|
||||
doctreedir = path.join(outdir, '.doctrees')
|
||||
for opt, val in opts:
|
||||
if opt == '-b':
|
||||
@@ -101,6 +103,8 @@ def main(argv):
|
||||
usage(argv, 'Cannot combine -a option and filenames.')
|
||||
return 1
|
||||
all_files = True
|
||||
elif opt == '-t':
|
||||
tags.append(val)
|
||||
elif opt == '-d':
|
||||
doctreedir = path.abspath(val)
|
||||
elif opt == '-c':
|
||||
@@ -152,7 +156,8 @@ def main(argv):
|
||||
|
||||
try:
|
||||
app = Sphinx(srcdir, confdir, outdir, doctreedir, buildername,
|
||||
confoverrides, status, warning, freshenv, warningiserror)
|
||||
confoverrides, status, warning, freshenv,
|
||||
warningiserror, tags)
|
||||
app.build(all_files, filenames)
|
||||
return app.statuscode
|
||||
except KeyboardInterrupt:
|
||||
|
||||
@@ -106,12 +106,13 @@ class Config(object):
|
||||
latex_preamble = ('', None),
|
||||
)
|
||||
|
||||
def __init__(self, dirname, filename, overrides):
|
||||
def __init__(self, dirname, filename, overrides, tags):
|
||||
self.overrides = overrides
|
||||
self.values = Config.config_values.copy()
|
||||
config = {}
|
||||
if dirname is not None:
|
||||
config['__file__'] = path.join(dirname, filename)
|
||||
config['tags'] = tags
|
||||
olddir = os.getcwd()
|
||||
try:
|
||||
os.chdir(dirname)
|
||||
|
||||
@@ -383,6 +383,23 @@ class ProductionList(Directive):
|
||||
return [node] + messages
|
||||
|
||||
|
||||
class TabularColumns(Directive):
|
||||
"""
|
||||
Directive to give an explicit tabulary column definition to LaTeX.
|
||||
"""
|
||||
|
||||
has_content = False
|
||||
required_arguments = 1
|
||||
optional_arguments = 0
|
||||
final_argument_whitespace = True
|
||||
option_spec = {}
|
||||
|
||||
def run(self):
|
||||
node = addnodes.tabular_col_spec()
|
||||
node['spec'] = self.arguments[0]
|
||||
return [node]
|
||||
|
||||
|
||||
class Glossary(Directive):
|
||||
"""
|
||||
Directive to create a glossary with cross-reference targets
|
||||
@@ -506,20 +523,23 @@ class HList(Directive):
|
||||
return [newnode]
|
||||
|
||||
|
||||
class TabularColumns(Directive):
|
||||
class Only(Directive):
|
||||
"""
|
||||
Directive to give an explicit tabulary column definition to LaTeX.
|
||||
Directive to only include text if the given tag(s) are enabled.
|
||||
"""
|
||||
|
||||
has_content = False
|
||||
has_content = True
|
||||
required_arguments = 1
|
||||
optional_arguments = 0
|
||||
final_argument_whitespace = True
|
||||
option_spec = {}
|
||||
|
||||
def run(self):
|
||||
node = addnodes.tabular_col_spec()
|
||||
node['spec'] = self.arguments[0]
|
||||
node = addnodes.only()
|
||||
node.document = self.state.document
|
||||
node.line = self.lineno
|
||||
node['expr'] = self.arguments[0]
|
||||
self.state.nested_parse(self.content, self.content_offset, node)
|
||||
return [node]
|
||||
|
||||
|
||||
@@ -535,11 +555,12 @@ directives.register_directive('versionadded', VersionChange)
|
||||
directives.register_directive('versionchanged', VersionChange)
|
||||
directives.register_directive('seealso', SeeAlso)
|
||||
directives.register_directive('productionlist', ProductionList)
|
||||
directives.register_directive('tabularcolumns', TabularColumns)
|
||||
directives.register_directive('glossary', Glossary)
|
||||
directives.register_directive('centered', Centered)
|
||||
directives.register_directive('acks', Acks)
|
||||
directives.register_directive('hlist', HList)
|
||||
directives.register_directive('tabularcolumns', TabularColumns)
|
||||
directives.register_directive('only', Only)
|
||||
|
||||
# register the standard rst class directive under a different name
|
||||
|
||||
|
||||
@@ -1270,6 +1270,19 @@ class BuildEnvironment:
|
||||
if newnode:
|
||||
node.replace_self(newnode)
|
||||
|
||||
for node in doctree.traverse(addnodes.only):
|
||||
try:
|
||||
ret = builder.tags.eval_condition(node['expr'])
|
||||
except Exception, err:
|
||||
self.warn(fromdocname, 'exception while evaluating only '
|
||||
'directive expression: %s' % err, node.line)
|
||||
node.replace_self(node.children)
|
||||
else:
|
||||
if ret:
|
||||
node.replace_self(node.children)
|
||||
else:
|
||||
node.replace_self([])
|
||||
|
||||
# allow custom references to be resolved
|
||||
builder.app.emit('doctree-resolved', doctree, fromdocname)
|
||||
|
||||
|
||||
90
sphinx/util/tags.py
Normal file
90
sphinx/util/tags.py
Normal file
@@ -0,0 +1,90 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
sphinx.util.tags
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
:copyright: Copyright 2007-2009 by the Sphinx team, see AUTHORS.
|
||||
:license: BSD, see LICENSE for details.
|
||||
"""
|
||||
|
||||
import warnings
|
||||
# jinja2.sandbox imports the sets module on purpose
|
||||
warnings.filterwarnings('ignore', 'the sets module', DeprecationWarning,
|
||||
module='jinja2.sandbox')
|
||||
|
||||
# (ab)use the Jinja parser for parsing our boolean expressions
|
||||
from jinja2 import nodes
|
||||
from jinja2.parser import Parser
|
||||
from jinja2.environment import Environment
|
||||
|
||||
env = Environment()
|
||||
|
||||
|
||||
class BooleanParser(Parser):
|
||||
"""
|
||||
Only allow condition exprs and/or/not operations.
|
||||
"""
|
||||
|
||||
def parse_compare(self):
|
||||
token = self.stream.current
|
||||
if token.type == 'name':
|
||||
if token.value in ('true', 'false', 'True', 'False'):
|
||||
node = nodes.Const(token.value in ('true', 'True'),
|
||||
lineno=token.lineno)
|
||||
elif token.value in ('none', 'None'):
|
||||
node = nodes.Const(None, lineno=token.lineno)
|
||||
else:
|
||||
node = nodes.Name(token.value, 'load', lineno=token.lineno)
|
||||
self.stream.next()
|
||||
elif token.type == 'lparen':
|
||||
self.stream.next()
|
||||
node = self.parse_expression()
|
||||
self.stream.expect('rparen')
|
||||
else:
|
||||
self.fail("unexpected token '%s'" % (token,), token.lineno)
|
||||
return node
|
||||
|
||||
|
||||
class Tags(object):
|
||||
def __init__(self, tags=None):
|
||||
self.tags = dict.fromkeys(tags or [], True)
|
||||
|
||||
def has(self, tag):
|
||||
return tag in self.tags
|
||||
|
||||
__contains__ = has
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.tags)
|
||||
|
||||
def add(self, tag):
|
||||
self.tags[tag] = True
|
||||
|
||||
def remove(self, tag):
|
||||
self.tags.pop(tag, None)
|
||||
|
||||
def eval_condition(self, condition):
|
||||
# exceptions are handled by the caller
|
||||
parser = BooleanParser(env, condition, state='variable')
|
||||
expr = parser.parse_expression()
|
||||
if not parser.stream.eos:
|
||||
raise ValueError('chunk after expression')
|
||||
|
||||
def eval_node(node):
|
||||
if isinstance(node, nodes.CondExpr):
|
||||
if eval_node(node.test):
|
||||
return eval_node(node.expr1)
|
||||
else:
|
||||
return eval_node(node.expr2)
|
||||
elif isinstance(node, nodes.And):
|
||||
return eval_node(node.left) and eval_node(node.right)
|
||||
elif isinstance(node, nodes.Or):
|
||||
return eval_node(node.left) or eval_node(node.right)
|
||||
elif isinstance(node, nodes.Not):
|
||||
return not eval_node(node.node)
|
||||
elif isinstance(node, nodes.Name):
|
||||
return self.tags.get(node.name, False)
|
||||
else:
|
||||
raise ValueError('invalid node, check parsing')
|
||||
|
||||
return eval_node(expr)
|
||||
@@ -47,6 +47,8 @@ value_from_conf_py = 84
|
||||
coverage_c_path = ['special/*.h']
|
||||
coverage_c_regexes = {'cfunction': r'^PyAPI_FUNC\(.*\)\s+([^_][\w_]+)'}
|
||||
|
||||
# modify tags from conf.py
|
||||
tags.add('confpytag')
|
||||
|
||||
from sphinx import addnodes
|
||||
|
||||
|
||||
@@ -176,6 +176,26 @@ Invalid index markup...
|
||||
Testing öäü...
|
||||
|
||||
|
||||
Only directive
|
||||
--------------
|
||||
|
||||
.. only:: html
|
||||
|
||||
In HTML.
|
||||
|
||||
.. only:: latex
|
||||
|
||||
In LaTeX.
|
||||
|
||||
.. only:: html or latex
|
||||
|
||||
In both.
|
||||
|
||||
.. only:: confpytag and (testtag or nonexisting_tag)
|
||||
|
||||
Always present, because set through conf.py/command line.
|
||||
|
||||
|
||||
.. rubric:: Footnotes
|
||||
|
||||
.. [#] Like footnotes.
|
||||
|
||||
@@ -83,6 +83,9 @@ HTML_XPATH = {
|
||||
".//div[@id='label']": '',
|
||||
".//span[@class='option']": '--help',
|
||||
".//p": 'A global substitution.',
|
||||
".//p": 'In HTML.',
|
||||
".//p": 'In both.',
|
||||
".//p": 'Always present',
|
||||
},
|
||||
'desc.html': {
|
||||
".//dt[@id='mod.Cls.meth1']": '',
|
||||
@@ -135,7 +138,7 @@ class NslessParser(ET.XMLParser):
|
||||
return name
|
||||
|
||||
|
||||
@with_app(buildername='html', warning=html_warnfile)
|
||||
@with_app(buildername='html', warning=html_warnfile, tags=['testtag'])
|
||||
def test_html(app):
|
||||
app.builder.build_all()
|
||||
html_warnings = html_warnfile.getvalue().replace(os.sep, '/')
|
||||
|
||||
@@ -99,8 +99,9 @@ class TestApp(application.Sphinx):
|
||||
|
||||
def __init__(self, srcdir=None, confdir=None, outdir=None, doctreedir=None,
|
||||
buildername='html', confoverrides=None,
|
||||
status=None, warning=None,
|
||||
freshenv=None, confname='conf.py', cleanenv=False):
|
||||
status=None, warning=None, freshenv=None,
|
||||
warningiserror=None, tags=None,
|
||||
confname='conf.py', cleanenv=False):
|
||||
|
||||
application.CONFIG_FILENAME = confname
|
||||
|
||||
@@ -136,10 +137,12 @@ class TestApp(application.Sphinx):
|
||||
warning = ListOutput('stderr')
|
||||
if freshenv is None:
|
||||
freshenv = False
|
||||
if warningiserror is None:
|
||||
warningiserror = False
|
||||
|
||||
application.Sphinx.__init__(self, srcdir, confdir, outdir, doctreedir,
|
||||
buildername, confoverrides, status, warning,
|
||||
freshenv)
|
||||
freshenv, warningiserror, tags)
|
||||
|
||||
def cleanup(self, doctrees=False):
|
||||
AutoDirective._registry.clear()
|
||||
|
||||
Reference in New Issue
Block a user