first commit
This commit is contained in:
commit
417e54da96
5696 changed files with 900003 additions and 0 deletions
|
@ -0,0 +1,466 @@
|
|||
# $Id: __init__.py 9426 2023-07-03 12:38:54Z milde $
|
||||
# Author: David Goodger <goodger@python.org>
|
||||
# Copyright: This module has been placed in the public domain.
|
||||
|
||||
"""
|
||||
This package contains directive implementation modules.
|
||||
"""
|
||||
|
||||
__docformat__ = 'reStructuredText'
|
||||
|
||||
import re
|
||||
import codecs
|
||||
from importlib import import_module
|
||||
|
||||
from docutils import nodes, parsers
|
||||
from docutils.utils import split_escaped_whitespace, escape2null
|
||||
from docutils.parsers.rst.languages import en as _fallback_language_module
|
||||
|
||||
|
||||
_directive_registry = {
|
||||
'attention': ('admonitions', 'Attention'),
|
||||
'caution': ('admonitions', 'Caution'),
|
||||
'code': ('body', 'CodeBlock'),
|
||||
'danger': ('admonitions', 'Danger'),
|
||||
'error': ('admonitions', 'Error'),
|
||||
'important': ('admonitions', 'Important'),
|
||||
'note': ('admonitions', 'Note'),
|
||||
'tip': ('admonitions', 'Tip'),
|
||||
'hint': ('admonitions', 'Hint'),
|
||||
'warning': ('admonitions', 'Warning'),
|
||||
'admonition': ('admonitions', 'Admonition'),
|
||||
'sidebar': ('body', 'Sidebar'),
|
||||
'topic': ('body', 'Topic'),
|
||||
'line-block': ('body', 'LineBlock'),
|
||||
'parsed-literal': ('body', 'ParsedLiteral'),
|
||||
'math': ('body', 'MathBlock'),
|
||||
'rubric': ('body', 'Rubric'),
|
||||
'epigraph': ('body', 'Epigraph'),
|
||||
'highlights': ('body', 'Highlights'),
|
||||
'pull-quote': ('body', 'PullQuote'),
|
||||
'compound': ('body', 'Compound'),
|
||||
'container': ('body', 'Container'),
|
||||
# 'questions': ('body', 'question_list'),
|
||||
'table': ('tables', 'RSTTable'),
|
||||
'csv-table': ('tables', 'CSVTable'),
|
||||
'list-table': ('tables', 'ListTable'),
|
||||
'image': ('images', 'Image'),
|
||||
'figure': ('images', 'Figure'),
|
||||
'contents': ('parts', 'Contents'),
|
||||
'sectnum': ('parts', 'Sectnum'),
|
||||
'header': ('parts', 'Header'),
|
||||
'footer': ('parts', 'Footer'),
|
||||
# 'footnotes': ('parts', 'footnotes'),
|
||||
# 'citations': ('parts', 'citations'),
|
||||
'target-notes': ('references', 'TargetNotes'),
|
||||
'meta': ('misc', 'Meta'),
|
||||
# 'imagemap': ('html', 'imagemap'),
|
||||
'raw': ('misc', 'Raw'),
|
||||
'include': ('misc', 'Include'),
|
||||
'replace': ('misc', 'Replace'),
|
||||
'unicode': ('misc', 'Unicode'),
|
||||
'class': ('misc', 'Class'),
|
||||
'role': ('misc', 'Role'),
|
||||
'default-role': ('misc', 'DefaultRole'),
|
||||
'title': ('misc', 'Title'),
|
||||
'date': ('misc', 'Date'),
|
||||
'restructuredtext-test-directive': ('misc', 'TestDirective'),
|
||||
}
|
||||
"""Mapping of directive name to (module name, class name). The
|
||||
directive name is canonical & must be lowercase. Language-dependent
|
||||
names are defined in the ``language`` subpackage."""
|
||||
|
||||
_directives = {}
|
||||
"""Cache of imported directives."""
|
||||
|
||||
|
||||
def directive(directive_name, language_module, document):
|
||||
"""
|
||||
Locate and return a directive function from its language-dependent name.
|
||||
If not found in the current language, check English. Return None if the
|
||||
named directive cannot be found.
|
||||
"""
|
||||
normname = directive_name.lower()
|
||||
messages = []
|
||||
msg_text = []
|
||||
if normname in _directives:
|
||||
return _directives[normname], messages
|
||||
canonicalname = None
|
||||
try:
|
||||
canonicalname = language_module.directives[normname]
|
||||
except AttributeError as error:
|
||||
msg_text.append('Problem retrieving directive entry from language '
|
||||
'module %r: %s.' % (language_module, error))
|
||||
except KeyError:
|
||||
msg_text.append('No directive entry for "%s" in module "%s".'
|
||||
% (directive_name, language_module.__name__))
|
||||
if not canonicalname:
|
||||
try:
|
||||
canonicalname = _fallback_language_module.directives[normname]
|
||||
msg_text.append('Using English fallback for directive "%s".'
|
||||
% directive_name)
|
||||
except KeyError:
|
||||
msg_text.append('Trying "%s" as canonical directive name.'
|
||||
% directive_name)
|
||||
# The canonical name should be an English name, but just in case:
|
||||
canonicalname = normname
|
||||
if msg_text:
|
||||
message = document.reporter.info(
|
||||
'\n'.join(msg_text), line=document.current_line)
|
||||
messages.append(message)
|
||||
try:
|
||||
modulename, classname = _directive_registry[canonicalname]
|
||||
except KeyError:
|
||||
# Error handling done by caller.
|
||||
return None, messages
|
||||
try:
|
||||
module = import_module('docutils.parsers.rst.directives.'+modulename)
|
||||
except ImportError as detail:
|
||||
messages.append(document.reporter.error(
|
||||
'Error importing directive module "%s" (directive "%s"):\n%s'
|
||||
% (modulename, directive_name, detail),
|
||||
line=document.current_line))
|
||||
return None, messages
|
||||
try:
|
||||
directive = getattr(module, classname)
|
||||
_directives[normname] = directive
|
||||
except AttributeError:
|
||||
messages.append(document.reporter.error(
|
||||
'No directive class "%s" in module "%s" (directive "%s").'
|
||||
% (classname, modulename, directive_name),
|
||||
line=document.current_line))
|
||||
return None, messages
|
||||
return directive, messages
|
||||
|
||||
|
||||
def register_directive(name, directive):
|
||||
"""
|
||||
Register a nonstandard application-defined directive function.
|
||||
Language lookups are not needed for such functions.
|
||||
"""
|
||||
_directives[name] = directive
|
||||
|
||||
|
||||
# conversion functions for `Directive.option_spec`
|
||||
# ------------------------------------------------
|
||||
#
|
||||
# see also `parsers.rst.Directive` in ../__init__.py.
|
||||
|
||||
|
||||
def flag(argument):
|
||||
"""
|
||||
Check for a valid flag option (no argument) and return ``None``.
|
||||
(Directive option conversion function.)
|
||||
|
||||
Raise ``ValueError`` if an argument is found.
|
||||
"""
|
||||
if argument and argument.strip():
|
||||
raise ValueError('no argument is allowed; "%s" supplied' % argument)
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def unchanged_required(argument):
|
||||
"""
|
||||
Return the argument text, unchanged.
|
||||
(Directive option conversion function.)
|
||||
|
||||
Raise ``ValueError`` if no argument is found.
|
||||
"""
|
||||
if argument is None:
|
||||
raise ValueError('argument required but none supplied')
|
||||
else:
|
||||
return argument # unchanged!
|
||||
|
||||
|
||||
def unchanged(argument):
|
||||
"""
|
||||
Return the argument text, unchanged.
|
||||
(Directive option conversion function.)
|
||||
|
||||
No argument implies empty string ("").
|
||||
"""
|
||||
if argument is None:
|
||||
return ''
|
||||
else:
|
||||
return argument # unchanged!
|
||||
|
||||
|
||||
def path(argument):
|
||||
"""
|
||||
Return the path argument unwrapped (with newlines removed).
|
||||
(Directive option conversion function.)
|
||||
|
||||
Raise ``ValueError`` if no argument is found.
|
||||
"""
|
||||
if argument is None:
|
||||
raise ValueError('argument required but none supplied')
|
||||
else:
|
||||
return ''.join(s.strip() for s in argument.splitlines())
|
||||
|
||||
|
||||
def uri(argument):
|
||||
"""
|
||||
Return the URI argument with unescaped whitespace removed.
|
||||
(Directive option conversion function.)
|
||||
|
||||
Raise ``ValueError`` if no argument is found.
|
||||
"""
|
||||
if argument is None:
|
||||
raise ValueError('argument required but none supplied')
|
||||
else:
|
||||
parts = split_escaped_whitespace(escape2null(argument))
|
||||
return ' '.join(''.join(nodes.unescape(part).split())
|
||||
for part in parts)
|
||||
|
||||
|
||||
def nonnegative_int(argument):
|
||||
"""
|
||||
Check for a nonnegative integer argument; raise ``ValueError`` if not.
|
||||
(Directive option conversion function.)
|
||||
"""
|
||||
value = int(argument)
|
||||
if value < 0:
|
||||
raise ValueError('negative value; must be positive or zero')
|
||||
return value
|
||||
|
||||
|
||||
def percentage(argument):
|
||||
"""
|
||||
Check for an integer percentage value with optional percent sign.
|
||||
(Directive option conversion function.)
|
||||
"""
|
||||
try:
|
||||
argument = argument.rstrip(' %')
|
||||
except AttributeError:
|
||||
pass
|
||||
return nonnegative_int(argument)
|
||||
|
||||
|
||||
length_units = ['em', 'ex', 'px', 'in', 'cm', 'mm', 'pt', 'pc']
|
||||
|
||||
|
||||
def get_measure(argument, units):
|
||||
"""
|
||||
Check for a positive argument of one of the units and return a
|
||||
normalized string of the form "<value><unit>" (without space in
|
||||
between).
|
||||
(Directive option conversion function.)
|
||||
|
||||
To be called from directive option conversion functions.
|
||||
"""
|
||||
match = re.match(r'^([0-9.]+) *(%s)$' % '|'.join(units), argument)
|
||||
try:
|
||||
float(match.group(1))
|
||||
except (AttributeError, ValueError):
|
||||
raise ValueError(
|
||||
'not a positive measure of one of the following units:\n%s'
|
||||
% ' '.join('"%s"' % i for i in units))
|
||||
return match.group(1) + match.group(2)
|
||||
|
||||
|
||||
def length_or_unitless(argument):
|
||||
return get_measure(argument, length_units + [''])
|
||||
|
||||
|
||||
def length_or_percentage_or_unitless(argument, default=''):
|
||||
"""
|
||||
Return normalized string of a length or percentage unit.
|
||||
(Directive option conversion function.)
|
||||
|
||||
Add <default> if there is no unit. Raise ValueError if the argument is not
|
||||
a positive measure of one of the valid CSS units (or without unit).
|
||||
|
||||
>>> length_or_percentage_or_unitless('3 pt')
|
||||
'3pt'
|
||||
>>> length_or_percentage_or_unitless('3%', 'em')
|
||||
'3%'
|
||||
>>> length_or_percentage_or_unitless('3')
|
||||
'3'
|
||||
>>> length_or_percentage_or_unitless('3', 'px')
|
||||
'3px'
|
||||
"""
|
||||
try:
|
||||
return get_measure(argument, length_units + ['%'])
|
||||
except ValueError:
|
||||
try:
|
||||
return get_measure(argument, ['']) + default
|
||||
except ValueError:
|
||||
# raise ValueError with list of valid units:
|
||||
return get_measure(argument, length_units + ['%'])
|
||||
|
||||
|
||||
def class_option(argument):
|
||||
"""
|
||||
Convert the argument into a list of ID-compatible strings and return it.
|
||||
(Directive option conversion function.)
|
||||
|
||||
Raise ``ValueError`` if no argument is found.
|
||||
"""
|
||||
if argument is None:
|
||||
raise ValueError('argument required but none supplied')
|
||||
names = argument.split()
|
||||
class_names = []
|
||||
for name in names:
|
||||
class_name = nodes.make_id(name)
|
||||
if not class_name:
|
||||
raise ValueError('cannot make "%s" into a class name' % name)
|
||||
class_names.append(class_name)
|
||||
return class_names
|
||||
|
||||
|
||||
unicode_pattern = re.compile(
|
||||
r'(?:0x|x|\\x|U\+?|\\u)([0-9a-f]+)$|&#x([0-9a-f]+);$', re.IGNORECASE)
|
||||
|
||||
|
||||
def unicode_code(code):
|
||||
r"""
|
||||
Convert a Unicode character code to a Unicode character.
|
||||
(Directive option conversion function.)
|
||||
|
||||
Codes may be decimal numbers, hexadecimal numbers (prefixed by ``0x``,
|
||||
``x``, ``\x``, ``U+``, ``u``, or ``\u``; e.g. ``U+262E``), or XML-style
|
||||
numeric character entities (e.g. ``☮``). Other text remains as-is.
|
||||
|
||||
Raise ValueError for illegal Unicode code values.
|
||||
"""
|
||||
try:
|
||||
if code.isdigit(): # decimal number
|
||||
return chr(int(code))
|
||||
else:
|
||||
match = unicode_pattern.match(code)
|
||||
if match: # hex number
|
||||
value = match.group(1) or match.group(2)
|
||||
return chr(int(value, 16))
|
||||
else: # other text
|
||||
return code
|
||||
except OverflowError as detail:
|
||||
raise ValueError('code too large (%s)' % detail)
|
||||
|
||||
|
||||
def single_char_or_unicode(argument):
|
||||
"""
|
||||
A single character is returned as-is. Unicode character codes are
|
||||
converted as in `unicode_code`. (Directive option conversion function.)
|
||||
"""
|
||||
char = unicode_code(argument)
|
||||
if len(char) > 1:
|
||||
raise ValueError('%r invalid; must be a single character or '
|
||||
'a Unicode code' % char)
|
||||
return char
|
||||
|
||||
|
||||
def single_char_or_whitespace_or_unicode(argument):
|
||||
"""
|
||||
As with `single_char_or_unicode`, but "tab" and "space" are also supported.
|
||||
(Directive option conversion function.)
|
||||
"""
|
||||
if argument == 'tab':
|
||||
char = '\t'
|
||||
elif argument == 'space':
|
||||
char = ' '
|
||||
else:
|
||||
char = single_char_or_unicode(argument)
|
||||
return char
|
||||
|
||||
|
||||
def positive_int(argument):
|
||||
"""
|
||||
Converts the argument into an integer. Raises ValueError for negative,
|
||||
zero, or non-integer values. (Directive option conversion function.)
|
||||
"""
|
||||
value = int(argument)
|
||||
if value < 1:
|
||||
raise ValueError('negative or zero value; must be positive')
|
||||
return value
|
||||
|
||||
|
||||
def positive_int_list(argument):
|
||||
"""
|
||||
Converts a space- or comma-separated list of values into a Python list
|
||||
of integers.
|
||||
(Directive option conversion function.)
|
||||
|
||||
Raises ValueError for non-positive-integer values.
|
||||
"""
|
||||
if ',' in argument:
|
||||
entries = argument.split(',')
|
||||
else:
|
||||
entries = argument.split()
|
||||
return [positive_int(entry) for entry in entries]
|
||||
|
||||
|
||||
def encoding(argument):
|
||||
"""
|
||||
Verifies the encoding argument by lookup.
|
||||
(Directive option conversion function.)
|
||||
|
||||
Raises ValueError for unknown encodings.
|
||||
"""
|
||||
try:
|
||||
codecs.lookup(argument)
|
||||
except LookupError:
|
||||
raise ValueError('unknown encoding: "%s"' % argument)
|
||||
return argument
|
||||
|
||||
|
||||
def choice(argument, values):
|
||||
"""
|
||||
Directive option utility function, supplied to enable options whose
|
||||
argument must be a member of a finite set of possible values (must be
|
||||
lower case). A custom conversion function must be written to use it. For
|
||||
example::
|
||||
|
||||
from docutils.parsers.rst import directives
|
||||
|
||||
def yesno(argument):
|
||||
return directives.choice(argument, ('yes', 'no'))
|
||||
|
||||
Raise ``ValueError`` if no argument is found or if the argument's value is
|
||||
not valid (not an entry in the supplied list).
|
||||
"""
|
||||
try:
|
||||
value = argument.lower().strip()
|
||||
except AttributeError:
|
||||
raise ValueError('must supply an argument; choose from %s'
|
||||
% format_values(values))
|
||||
if value in values:
|
||||
return value
|
||||
else:
|
||||
raise ValueError('"%s" unknown; choose from %s'
|
||||
% (argument, format_values(values)))
|
||||
|
||||
|
||||
def format_values(values):
|
||||
return '%s, or "%s"' % (', '.join('"%s"' % s for s in values[:-1]),
|
||||
values[-1])
|
||||
|
||||
|
||||
def value_or(values, other):
|
||||
"""
|
||||
Directive option conversion function.
|
||||
|
||||
The argument can be any of `values` or `argument_type`.
|
||||
"""
|
||||
def auto_or_other(argument):
|
||||
if argument in values:
|
||||
return argument
|
||||
else:
|
||||
return other(argument)
|
||||
return auto_or_other
|
||||
|
||||
|
||||
def parser_name(argument):
|
||||
"""
|
||||
Return a docutils parser whose name matches the argument.
|
||||
(Directive option conversion function.)
|
||||
|
||||
Return `None`, if the argument evaluates to `False`.
|
||||
Raise `ValueError` if importing the parser module fails.
|
||||
"""
|
||||
if not argument:
|
||||
return None
|
||||
try:
|
||||
return parsers.get_parser_class(argument)
|
||||
except ImportError as err:
|
||||
raise ValueError(str(err))
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,101 @@
|
|||
# $Id: admonitions.py 9475 2023-11-13 22:30:00Z milde $
|
||||
# Author: David Goodger <goodger@python.org>
|
||||
# Copyright: This module has been placed in the public domain.
|
||||
|
||||
"""
|
||||
Admonition directives.
|
||||
"""
|
||||
|
||||
__docformat__ = 'reStructuredText'
|
||||
|
||||
|
||||
from docutils.parsers.rst import Directive
|
||||
from docutils.parsers.rst import directives
|
||||
from docutils.parsers.rst.roles import set_classes
|
||||
from docutils import nodes
|
||||
|
||||
|
||||
class BaseAdmonition(Directive):
|
||||
|
||||
final_argument_whitespace = True
|
||||
option_spec = {'class': directives.class_option,
|
||||
'name': directives.unchanged}
|
||||
has_content = True
|
||||
|
||||
node_class = None
|
||||
"""Subclasses must set this to the appropriate admonition node class."""
|
||||
|
||||
def run(self):
|
||||
set_classes(self.options)
|
||||
self.assert_has_content()
|
||||
text = '\n'.join(self.content)
|
||||
admonition_node = self.node_class(text, **self.options)
|
||||
self.add_name(admonition_node)
|
||||
admonition_node.source, admonition_node.line = \
|
||||
self.state_machine.get_source_and_line(self.lineno)
|
||||
if self.node_class is nodes.admonition:
|
||||
title_text = self.arguments[0]
|
||||
textnodes, messages = self.state.inline_text(title_text,
|
||||
self.lineno)
|
||||
title = nodes.title(title_text, '', *textnodes)
|
||||
title.source, title.line = (
|
||||
self.state_machine.get_source_and_line(self.lineno))
|
||||
admonition_node += title
|
||||
admonition_node += messages
|
||||
if 'classes' not in self.options:
|
||||
admonition_node['classes'] += ['admonition-'
|
||||
+ nodes.make_id(title_text)]
|
||||
self.state.nested_parse(self.content, self.content_offset,
|
||||
admonition_node)
|
||||
return [admonition_node]
|
||||
|
||||
|
||||
class Admonition(BaseAdmonition):
|
||||
|
||||
required_arguments = 1
|
||||
node_class = nodes.admonition
|
||||
|
||||
|
||||
class Attention(BaseAdmonition):
|
||||
|
||||
node_class = nodes.attention
|
||||
|
||||
|
||||
class Caution(BaseAdmonition):
|
||||
|
||||
node_class = nodes.caution
|
||||
|
||||
|
||||
class Danger(BaseAdmonition):
|
||||
|
||||
node_class = nodes.danger
|
||||
|
||||
|
||||
class Error(BaseAdmonition):
|
||||
|
||||
node_class = nodes.error
|
||||
|
||||
|
||||
class Hint(BaseAdmonition):
|
||||
|
||||
node_class = nodes.hint
|
||||
|
||||
|
||||
class Important(BaseAdmonition):
|
||||
|
||||
node_class = nodes.important
|
||||
|
||||
|
||||
class Note(BaseAdmonition):
|
||||
|
||||
node_class = nodes.note
|
||||
|
||||
|
||||
class Tip(BaseAdmonition):
|
||||
|
||||
node_class = nodes.tip
|
||||
|
||||
|
||||
class Warning(BaseAdmonition):
|
||||
|
||||
node_class = nodes.warning
|
|
@ -0,0 +1,305 @@
|
|||
# $Id: body.py 9500 2023-12-14 22:38:49Z milde $
|
||||
# Author: David Goodger <goodger@python.org>
|
||||
# Copyright: This module has been placed in the public domain.
|
||||
|
||||
"""
|
||||
Directives for additional body elements.
|
||||
|
||||
See `docutils.parsers.rst.directives` for API details.
|
||||
"""
|
||||
|
||||
__docformat__ = 'reStructuredText'
|
||||
|
||||
|
||||
from docutils import nodes
|
||||
from docutils.parsers.rst import Directive
|
||||
from docutils.parsers.rst import directives
|
||||
from docutils.parsers.rst.roles import set_classes
|
||||
from docutils.utils.code_analyzer import Lexer, LexerError, NumberLines
|
||||
|
||||
|
||||
class BasePseudoSection(Directive):
|
||||
|
||||
required_arguments = 1
|
||||
optional_arguments = 0
|
||||
final_argument_whitespace = True
|
||||
option_spec = {'class': directives.class_option,
|
||||
'name': directives.unchanged}
|
||||
has_content = True
|
||||
|
||||
node_class = None
|
||||
"""Node class to be used (must be set in subclasses)."""
|
||||
|
||||
def run(self):
|
||||
if not (self.state_machine.match_titles
|
||||
or isinstance(self.state_machine.node, nodes.sidebar)):
|
||||
raise self.error('The "%s" directive may not be used within '
|
||||
'topics or body elements.' % self.name)
|
||||
self.assert_has_content()
|
||||
if self.arguments: # title (in sidebars optional)
|
||||
title_text = self.arguments[0]
|
||||
textnodes, messages = self.state.inline_text(
|
||||
title_text, self.lineno)
|
||||
titles = [nodes.title(title_text, '', *textnodes)]
|
||||
# Sidebar uses this code.
|
||||
if 'subtitle' in self.options:
|
||||
textnodes, more_messages = self.state.inline_text(
|
||||
self.options['subtitle'], self.lineno)
|
||||
titles.append(nodes.subtitle(self.options['subtitle'], '',
|
||||
*textnodes))
|
||||
messages.extend(more_messages)
|
||||
else:
|
||||
titles = []
|
||||
messages = []
|
||||
text = '\n'.join(self.content)
|
||||
node = self.node_class(text, *(titles + messages))
|
||||
node['classes'] += self.options.get('class', [])
|
||||
self.add_name(node)
|
||||
if text:
|
||||
self.state.nested_parse(self.content, self.content_offset, node)
|
||||
return [node]
|
||||
|
||||
|
||||
class Topic(BasePseudoSection):
|
||||
|
||||
node_class = nodes.topic
|
||||
|
||||
|
||||
class Sidebar(BasePseudoSection):
|
||||
|
||||
node_class = nodes.sidebar
|
||||
|
||||
required_arguments = 0
|
||||
optional_arguments = 1
|
||||
option_spec = BasePseudoSection.option_spec.copy()
|
||||
option_spec['subtitle'] = directives.unchanged_required
|
||||
|
||||
def run(self):
|
||||
if isinstance(self.state_machine.node, nodes.sidebar):
|
||||
raise self.error('The "%s" directive may not be used within a '
|
||||
'sidebar element.' % self.name)
|
||||
if 'subtitle' in self.options and not self.arguments:
|
||||
raise self.error('The "subtitle" option may not be used '
|
||||
'without a title.')
|
||||
|
||||
return BasePseudoSection.run(self)
|
||||
|
||||
|
||||
class LineBlock(Directive):
|
||||
|
||||
option_spec = {'class': directives.class_option,
|
||||
'name': directives.unchanged}
|
||||
has_content = True
|
||||
|
||||
def run(self):
|
||||
self.assert_has_content()
|
||||
block = nodes.line_block(classes=self.options.get('class', []))
|
||||
self.add_name(block)
|
||||
node_list = [block]
|
||||
for line_text in self.content:
|
||||
text_nodes, messages = self.state.inline_text(
|
||||
line_text.strip(), self.lineno + self.content_offset)
|
||||
line = nodes.line(line_text, '', *text_nodes)
|
||||
if line_text.strip():
|
||||
line.indent = len(line_text) - len(line_text.lstrip())
|
||||
block += line
|
||||
node_list.extend(messages)
|
||||
self.content_offset += 1
|
||||
self.state.nest_line_block_lines(block)
|
||||
return node_list
|
||||
|
||||
|
||||
class ParsedLiteral(Directive):
|
||||
|
||||
option_spec = {'class': directives.class_option,
|
||||
'name': directives.unchanged}
|
||||
has_content = True
|
||||
|
||||
def run(self):
|
||||
set_classes(self.options)
|
||||
self.assert_has_content()
|
||||
text = '\n'.join(self.content)
|
||||
text_nodes, messages = self.state.inline_text(text, self.lineno)
|
||||
node = nodes.literal_block(text, '', *text_nodes, **self.options)
|
||||
node.line = self.content_offset + 1
|
||||
self.add_name(node)
|
||||
return [node] + messages
|
||||
|
||||
|
||||
class CodeBlock(Directive):
|
||||
"""Parse and mark up content of a code block.
|
||||
|
||||
Configuration setting: syntax_highlight
|
||||
Highlight Code content with Pygments?
|
||||
Possible values: ('long', 'short', 'none')
|
||||
|
||||
"""
|
||||
optional_arguments = 1
|
||||
option_spec = {'class': directives.class_option,
|
||||
'name': directives.unchanged,
|
||||
'number-lines': directives.unchanged # integer or None
|
||||
}
|
||||
has_content = True
|
||||
|
||||
def run(self):
|
||||
self.assert_has_content()
|
||||
if self.arguments:
|
||||
language = self.arguments[0]
|
||||
else:
|
||||
language = ''
|
||||
set_classes(self.options)
|
||||
classes = ['code']
|
||||
if language:
|
||||
classes.append(language)
|
||||
if 'classes' in self.options:
|
||||
classes.extend(self.options['classes'])
|
||||
|
||||
# set up lexical analyzer
|
||||
try:
|
||||
tokens = Lexer('\n'.join(self.content), language,
|
||||
self.state.document.settings.syntax_highlight)
|
||||
except LexerError as error:
|
||||
if self.state.document.settings.report_level > 2:
|
||||
# don't report warnings -> insert without syntax highlight
|
||||
tokens = Lexer('\n'.join(self.content), language, 'none')
|
||||
else:
|
||||
raise self.warning(error)
|
||||
|
||||
if 'number-lines' in self.options:
|
||||
# optional argument `startline`, defaults to 1
|
||||
try:
|
||||
startline = int(self.options['number-lines'] or 1)
|
||||
except ValueError:
|
||||
raise self.error(':number-lines: with non-integer start value')
|
||||
endline = startline + len(self.content)
|
||||
# add linenumber filter:
|
||||
tokens = NumberLines(tokens, startline, endline)
|
||||
|
||||
node = nodes.literal_block('\n'.join(self.content), classes=classes)
|
||||
self.add_name(node)
|
||||
# if called from "include", set the source
|
||||
if 'source' in self.options:
|
||||
node.attributes['source'] = self.options['source']
|
||||
# analyze content and add nodes for every token
|
||||
for classes, value in tokens:
|
||||
if classes:
|
||||
node += nodes.inline(value, value, classes=classes)
|
||||
else:
|
||||
# insert as Text to decrease the verbosity of the output
|
||||
node += nodes.Text(value)
|
||||
|
||||
return [node]
|
||||
|
||||
|
||||
class MathBlock(Directive):
|
||||
|
||||
option_spec = {'class': directives.class_option,
|
||||
'name': directives.unchanged,
|
||||
# TODO: Add Sphinx' ``mathbase.py`` option 'nowrap'?
|
||||
# 'nowrap': directives.flag,
|
||||
}
|
||||
has_content = True
|
||||
|
||||
def run(self):
|
||||
set_classes(self.options)
|
||||
self.assert_has_content()
|
||||
# join lines, separate blocks
|
||||
content = '\n'.join(self.content).split('\n\n')
|
||||
_nodes = []
|
||||
for block in content:
|
||||
if not block:
|
||||
continue
|
||||
node = nodes.math_block(self.block_text, block, **self.options)
|
||||
(node.source,
|
||||
node.line) = self.state_machine.get_source_and_line(self.lineno)
|
||||
self.add_name(node)
|
||||
_nodes.append(node)
|
||||
return _nodes
|
||||
|
||||
|
||||
class Rubric(Directive):
|
||||
|
||||
required_arguments = 1
|
||||
optional_arguments = 0
|
||||
final_argument_whitespace = True
|
||||
option_spec = {'class': directives.class_option,
|
||||
'name': directives.unchanged}
|
||||
|
||||
def run(self):
|
||||
set_classes(self.options)
|
||||
rubric_text = self.arguments[0]
|
||||
textnodes, messages = self.state.inline_text(rubric_text, self.lineno)
|
||||
rubric = nodes.rubric(rubric_text, '', *textnodes, **self.options)
|
||||
self.add_name(rubric)
|
||||
return [rubric] + messages
|
||||
|
||||
|
||||
class BlockQuote(Directive):
|
||||
|
||||
has_content = True
|
||||
classes = []
|
||||
|
||||
def run(self):
|
||||
self.assert_has_content()
|
||||
elements = self.state.block_quote(self.content, self.content_offset)
|
||||
for element in elements:
|
||||
if isinstance(element, nodes.block_quote):
|
||||
element['classes'] += self.classes
|
||||
return elements
|
||||
|
||||
|
||||
class Epigraph(BlockQuote):
|
||||
|
||||
classes = ['epigraph']
|
||||
|
||||
|
||||
class Highlights(BlockQuote):
|
||||
|
||||
classes = ['highlights']
|
||||
|
||||
|
||||
class PullQuote(BlockQuote):
|
||||
|
||||
classes = ['pull-quote']
|
||||
|
||||
|
||||
class Compound(Directive):
|
||||
|
||||
option_spec = {'class': directives.class_option,
|
||||
'name': directives.unchanged}
|
||||
has_content = True
|
||||
|
||||
def run(self):
|
||||
self.assert_has_content()
|
||||
text = '\n'.join(self.content)
|
||||
node = nodes.compound(text)
|
||||
node['classes'] += self.options.get('class', [])
|
||||
self.add_name(node)
|
||||
self.state.nested_parse(self.content, self.content_offset, node)
|
||||
return [node]
|
||||
|
||||
|
||||
class Container(Directive):
|
||||
|
||||
optional_arguments = 1
|
||||
final_argument_whitespace = True
|
||||
option_spec = {'name': directives.unchanged}
|
||||
has_content = True
|
||||
|
||||
def run(self):
|
||||
self.assert_has_content()
|
||||
text = '\n'.join(self.content)
|
||||
try:
|
||||
if self.arguments:
|
||||
classes = directives.class_option(self.arguments[0])
|
||||
else:
|
||||
classes = []
|
||||
except ValueError:
|
||||
raise self.error(
|
||||
'Invalid class attribute value for "%s" directive: "%s".'
|
||||
% (self.name, self.arguments[0]))
|
||||
node = nodes.container(text)
|
||||
node['classes'].extend(classes)
|
||||
self.add_name(node)
|
||||
self.state.nested_parse(self.content, self.content_offset, node)
|
||||
return [node]
|
|
@ -0,0 +1,21 @@
|
|||
# $Id: html.py 9062 2022-05-30 21:09:09Z milde $
|
||||
# Author: David Goodger <goodger@python.org>
|
||||
# Copyright: This module has been placed in the public domain.
|
||||
|
||||
"""
|
||||
Dummy module for backwards compatibility.
|
||||
|
||||
This module is provisional: it will be removed in Docutils 2.0.
|
||||
"""
|
||||
|
||||
__docformat__ = 'reStructuredText'
|
||||
|
||||
import warnings
|
||||
|
||||
from docutils.parsers.rst.directives.misc import MetaBody, Meta # noqa: F401
|
||||
|
||||
warnings.warn('The `docutils.parsers.rst.directive.html` module'
|
||||
' will be removed in Docutils 2.0.'
|
||||
' Since Docutils 0.18, the "Meta" node is defined in'
|
||||
' `docutils.parsers.rst.directives.misc`.',
|
||||
DeprecationWarning, stacklevel=2)
|
|
@ -0,0 +1,173 @@
|
|||
# $Id: images.py 9500 2023-12-14 22:38:49Z milde $
|
||||
# Author: David Goodger <goodger@python.org>
|
||||
# Copyright: This module has been placed in the public domain.
|
||||
|
||||
"""
|
||||
Directives for figures and simple images.
|
||||
"""
|
||||
|
||||
__docformat__ = 'reStructuredText'
|
||||
|
||||
from urllib.request import url2pathname
|
||||
|
||||
try: # check for the Python Imaging Library
|
||||
import PIL.Image
|
||||
except ImportError:
|
||||
try: # sometimes PIL modules are put in PYTHONPATH's root
|
||||
import Image
|
||||
class PIL: pass # noqa:E701 dummy wrapper
|
||||
PIL.Image = Image
|
||||
except ImportError:
|
||||
PIL = None
|
||||
|
||||
from docutils import nodes
|
||||
from docutils.nodes import fully_normalize_name, whitespace_normalize_name
|
||||
from docutils.parsers.rst import Directive
|
||||
from docutils.parsers.rst import directives, states
|
||||
from docutils.parsers.rst.roles import set_classes
|
||||
|
||||
|
||||
class Image(Directive):
|
||||
|
||||
align_h_values = ('left', 'center', 'right')
|
||||
align_v_values = ('top', 'middle', 'bottom')
|
||||
align_values = align_v_values + align_h_values
|
||||
loading_values = ('embed', 'link', 'lazy')
|
||||
|
||||
def align(argument):
|
||||
# This is not callable as `self.align()`. We cannot make it a
|
||||
# staticmethod because we're saving an unbound method in
|
||||
# option_spec below.
|
||||
return directives.choice(argument, Image.align_values)
|
||||
|
||||
def loading(argument):
|
||||
# This is not callable as `self.loading()` (see above).
|
||||
return directives.choice(argument, Image.loading_values)
|
||||
|
||||
required_arguments = 1
|
||||
optional_arguments = 0
|
||||
final_argument_whitespace = True
|
||||
option_spec = {'alt': directives.unchanged,
|
||||
'height': directives.length_or_unitless,
|
||||
'width': directives.length_or_percentage_or_unitless,
|
||||
'scale': directives.percentage,
|
||||
'align': align,
|
||||
'target': directives.unchanged_required,
|
||||
'loading': loading,
|
||||
'class': directives.class_option,
|
||||
'name': directives.unchanged}
|
||||
|
||||
def run(self):
|
||||
if 'align' in self.options:
|
||||
if isinstance(self.state, states.SubstitutionDef):
|
||||
# Check for align_v_values.
|
||||
if self.options['align'] not in self.align_v_values:
|
||||
raise self.error(
|
||||
'Error in "%s" directive: "%s" is not a valid value '
|
||||
'for the "align" option within a substitution '
|
||||
'definition. Valid values for "align" are: "%s".'
|
||||
% (self.name, self.options['align'],
|
||||
'", "'.join(self.align_v_values)))
|
||||
elif self.options['align'] not in self.align_h_values:
|
||||
raise self.error(
|
||||
'Error in "%s" directive: "%s" is not a valid value for '
|
||||
'the "align" option. Valid values for "align" are: "%s".'
|
||||
% (self.name, self.options['align'],
|
||||
'", "'.join(self.align_h_values)))
|
||||
messages = []
|
||||
reference = directives.uri(self.arguments[0])
|
||||
self.options['uri'] = reference
|
||||
reference_node = None
|
||||
if 'target' in self.options:
|
||||
block = states.escape2null(
|
||||
self.options['target']).splitlines()
|
||||
block = [line for line in block]
|
||||
target_type, data = self.state.parse_target(
|
||||
block, self.block_text, self.lineno)
|
||||
if target_type == 'refuri':
|
||||
reference_node = nodes.reference(refuri=data)
|
||||
elif target_type == 'refname':
|
||||
reference_node = nodes.reference(
|
||||
refname=fully_normalize_name(data),
|
||||
name=whitespace_normalize_name(data))
|
||||
reference_node.indirect_reference_name = data
|
||||
self.state.document.note_refname(reference_node)
|
||||
else: # malformed target
|
||||
messages.append(data) # data is a system message
|
||||
del self.options['target']
|
||||
set_classes(self.options)
|
||||
image_node = nodes.image(self.block_text, **self.options)
|
||||
(image_node.source,
|
||||
image_node.line) = self.state_machine.get_source_and_line(self.lineno)
|
||||
self.add_name(image_node)
|
||||
if reference_node:
|
||||
reference_node += image_node
|
||||
return messages + [reference_node]
|
||||
else:
|
||||
return messages + [image_node]
|
||||
|
||||
|
||||
class Figure(Image):
|
||||
|
||||
def align(argument):
|
||||
return directives.choice(argument, Figure.align_h_values)
|
||||
|
||||
def figwidth_value(argument):
|
||||
if argument.lower() == 'image':
|
||||
return 'image'
|
||||
else:
|
||||
return directives.length_or_percentage_or_unitless(argument, 'px')
|
||||
|
||||
option_spec = Image.option_spec.copy()
|
||||
option_spec['figwidth'] = figwidth_value
|
||||
option_spec['figclass'] = directives.class_option
|
||||
option_spec['align'] = align
|
||||
has_content = True
|
||||
|
||||
def run(self):
|
||||
figwidth = self.options.pop('figwidth', None)
|
||||
figclasses = self.options.pop('figclass', None)
|
||||
align = self.options.pop('align', None)
|
||||
(image_node,) = Image.run(self)
|
||||
if isinstance(image_node, nodes.system_message):
|
||||
return [image_node]
|
||||
figure_node = nodes.figure('', image_node)
|
||||
(figure_node.source, figure_node.line
|
||||
) = self.state_machine.get_source_and_line(self.lineno)
|
||||
if figwidth == 'image':
|
||||
if PIL and self.state.document.settings.file_insertion_enabled:
|
||||
imagepath = url2pathname(image_node['uri'])
|
||||
try:
|
||||
with PIL.Image.open(imagepath) as img:
|
||||
figure_node['width'] = '%dpx' % img.size[0]
|
||||
except (OSError, UnicodeEncodeError):
|
||||
pass # TODO: warn/info?
|
||||
else:
|
||||
self.state.document.settings.record_dependencies.add(
|
||||
imagepath.replace('\\', '/'))
|
||||
elif figwidth is not None:
|
||||
figure_node['width'] = figwidth
|
||||
if figclasses:
|
||||
figure_node['classes'] += figclasses
|
||||
if align:
|
||||
figure_node['align'] = align
|
||||
if self.content:
|
||||
node = nodes.Element() # anonymous container for parsing
|
||||
self.state.nested_parse(self.content, self.content_offset, node)
|
||||
first_node = node[0]
|
||||
if isinstance(first_node, nodes.paragraph):
|
||||
caption = nodes.caption(first_node.rawsource, '',
|
||||
*first_node.children)
|
||||
caption.source = first_node.source
|
||||
caption.line = first_node.line
|
||||
figure_node += caption
|
||||
elif not (isinstance(first_node, nodes.comment)
|
||||
and len(first_node) == 0):
|
||||
error = self.reporter.error(
|
||||
'Figure caption must be a paragraph or empty comment.',
|
||||
nodes.literal_block(self.block_text, self.block_text),
|
||||
line=self.lineno)
|
||||
return [figure_node, error]
|
||||
if len(node) > 1:
|
||||
figure_node += nodes.legend('', *node[1:])
|
||||
return [figure_node]
|
|
@ -0,0 +1,642 @@
|
|||
# $Id: misc.py 9492 2023-11-29 16:58:13Z milde $
|
||||
# Authors: David Goodger <goodger@python.org>; Dethe Elza
|
||||
# Copyright: This module has been placed in the public domain.
|
||||
|
||||
"""Miscellaneous directives."""
|
||||
|
||||
__docformat__ = 'reStructuredText'
|
||||
|
||||
from pathlib import Path
|
||||
import re
|
||||
import time
|
||||
from urllib.request import urlopen
|
||||
from urllib.error import URLError
|
||||
|
||||
from docutils import io, nodes, statemachine, utils
|
||||
from docutils.parsers.rst import Directive, convert_directive_function
|
||||
from docutils.parsers.rst import directives, roles, states
|
||||
from docutils.parsers.rst.directives.body import CodeBlock, NumberLines
|
||||
from docutils.transforms import misc
|
||||
|
||||
|
||||
def adapt_path(path, source='', root_prefix='/'):
|
||||
# Adapt path to files to include or embed.
|
||||
# `root_prefix` is prepended to absolute paths (cf. root_prefix setting),
|
||||
# `source` is the `current_source` of the including directive (which may
|
||||
# be a file included by the main document).
|
||||
if path.startswith('/'):
|
||||
base = Path(root_prefix)
|
||||
path = path[1:]
|
||||
else:
|
||||
base = Path(source).parent
|
||||
# pepend "base" and convert to relative path for shorter system messages
|
||||
return utils.relative_path(None, base/path)
|
||||
|
||||
|
||||
class Include(Directive):
|
||||
|
||||
"""
|
||||
Include content read from a separate source file.
|
||||
|
||||
Content may be parsed by the parser, or included as a literal
|
||||
block. The encoding of the included file can be specified. Only
|
||||
a part of the given file argument may be included by specifying
|
||||
start and end line or text to match before and/or after the text
|
||||
to be used.
|
||||
|
||||
https://docutils.sourceforge.io/docs/ref/rst/directives.html#including-an-external-document-fragment
|
||||
"""
|
||||
|
||||
required_arguments = 1
|
||||
optional_arguments = 0
|
||||
final_argument_whitespace = True
|
||||
option_spec = {'literal': directives.flag,
|
||||
'code': directives.unchanged,
|
||||
'encoding': directives.encoding,
|
||||
'parser': directives.parser_name,
|
||||
'tab-width': int,
|
||||
'start-line': int,
|
||||
'end-line': int,
|
||||
'start-after': directives.unchanged_required,
|
||||
'end-before': directives.unchanged_required,
|
||||
# ignored except for 'literal' or 'code':
|
||||
'number-lines': directives.unchanged, # integer or None
|
||||
'class': directives.class_option,
|
||||
'name': directives.unchanged}
|
||||
|
||||
standard_include_path = Path(states.__file__).parent / 'include'
|
||||
|
||||
def run(self):
|
||||
"""Include a file as part of the content of this reST file.
|
||||
|
||||
Depending on the options, the file (or a clipping) is
|
||||
converted to nodes and returned or inserted into the input stream.
|
||||
"""
|
||||
settings = self.state.document.settings
|
||||
if not settings.file_insertion_enabled:
|
||||
raise self.warning('"%s" directive disabled.' % self.name)
|
||||
tab_width = self.options.get('tab-width', settings.tab_width)
|
||||
current_source = self.state.document.current_source
|
||||
path = directives.path(self.arguments[0])
|
||||
if path.startswith('<') and path.endswith('>'):
|
||||
path = '/' + path[1:-1]
|
||||
root_prefix = self.standard_include_path
|
||||
else:
|
||||
root_prefix = settings.root_prefix
|
||||
path = adapt_path(path, current_source, root_prefix)
|
||||
encoding = self.options.get('encoding', settings.input_encoding)
|
||||
error_handler = settings.input_encoding_error_handler
|
||||
try:
|
||||
include_file = io.FileInput(source_path=path,
|
||||
encoding=encoding,
|
||||
error_handler=error_handler)
|
||||
except UnicodeEncodeError:
|
||||
raise self.severe(f'Problems with "{self.name}" directive path:\n'
|
||||
f'Cannot encode input file path "{path}" '
|
||||
'(wrong locale?).')
|
||||
except OSError as error:
|
||||
raise self.severe(f'Problems with "{self.name}" directive '
|
||||
f'path:\n{io.error_string(error)}.')
|
||||
else:
|
||||
settings.record_dependencies.add(path)
|
||||
|
||||
# Get to-be-included content
|
||||
startline = self.options.get('start-line', None)
|
||||
endline = self.options.get('end-line', None)
|
||||
try:
|
||||
if startline or (endline is not None):
|
||||
lines = include_file.readlines()
|
||||
rawtext = ''.join(lines[startline:endline])
|
||||
else:
|
||||
rawtext = include_file.read()
|
||||
except UnicodeError as error:
|
||||
raise self.severe(f'Problem with "{self.name}" directive:\n'
|
||||
+ io.error_string(error))
|
||||
# start-after/end-before: no restrictions on newlines in match-text,
|
||||
# and no restrictions on matching inside lines vs. line boundaries
|
||||
after_text = self.options.get('start-after', None)
|
||||
if after_text:
|
||||
# skip content in rawtext before *and incl.* a matching text
|
||||
after_index = rawtext.find(after_text)
|
||||
if after_index < 0:
|
||||
raise self.severe('Problem with "start-after" option of "%s" '
|
||||
'directive:\nText not found.' % self.name)
|
||||
rawtext = rawtext[after_index + len(after_text):]
|
||||
before_text = self.options.get('end-before', None)
|
||||
if before_text:
|
||||
# skip content in rawtext after *and incl.* a matching text
|
||||
before_index = rawtext.find(before_text)
|
||||
if before_index < 0:
|
||||
raise self.severe('Problem with "end-before" option of "%s" '
|
||||
'directive:\nText not found.' % self.name)
|
||||
rawtext = rawtext[:before_index]
|
||||
|
||||
include_lines = statemachine.string2lines(rawtext, tab_width,
|
||||
convert_whitespace=True)
|
||||
for i, line in enumerate(include_lines):
|
||||
if len(line) > settings.line_length_limit:
|
||||
raise self.warning('"%s": line %d exceeds the'
|
||||
' line-length-limit.' % (path, i+1))
|
||||
|
||||
if 'literal' in self.options:
|
||||
# Don't convert tabs to spaces, if `tab_width` is negative.
|
||||
if tab_width >= 0:
|
||||
text = rawtext.expandtabs(tab_width)
|
||||
else:
|
||||
text = rawtext
|
||||
literal_block = nodes.literal_block(
|
||||
rawtext, source=path,
|
||||
classes=self.options.get('class', []))
|
||||
literal_block.line = 1
|
||||
self.add_name(literal_block)
|
||||
if 'number-lines' in self.options:
|
||||
try:
|
||||
startline = int(self.options['number-lines'] or 1)
|
||||
except ValueError:
|
||||
raise self.error(':number-lines: with non-integer '
|
||||
'start value')
|
||||
endline = startline + len(include_lines)
|
||||
if text.endswith('\n'):
|
||||
text = text[:-1]
|
||||
tokens = NumberLines([([], text)], startline, endline)
|
||||
for classes, value in tokens:
|
||||
if classes:
|
||||
literal_block += nodes.inline(value, value,
|
||||
classes=classes)
|
||||
else:
|
||||
literal_block += nodes.Text(value)
|
||||
else:
|
||||
literal_block += nodes.Text(text)
|
||||
return [literal_block]
|
||||
|
||||
if 'code' in self.options:
|
||||
self.options['source'] = path
|
||||
# Don't convert tabs to spaces, if `tab_width` is negative:
|
||||
if tab_width < 0:
|
||||
include_lines = rawtext.splitlines()
|
||||
codeblock = CodeBlock(self.name,
|
||||
[self.options.pop('code')], # arguments
|
||||
self.options,
|
||||
include_lines, # content
|
||||
self.lineno,
|
||||
self.content_offset,
|
||||
self.block_text,
|
||||
self.state,
|
||||
self.state_machine)
|
||||
return codeblock.run()
|
||||
|
||||
# Prevent circular inclusion:
|
||||
clip_options = (startline, endline, before_text, after_text)
|
||||
include_log = self.state.document.include_log
|
||||
# log entries are tuples (<source>, <clip-options>)
|
||||
if not include_log: # new document, initialize with document source
|
||||
include_log.append((utils.relative_path(None, current_source),
|
||||
(None, None, None, None)))
|
||||
if (path, clip_options) in include_log:
|
||||
master_paths = (pth for (pth, opt) in reversed(include_log))
|
||||
inclusion_chain = '\n> '.join((path, *master_paths))
|
||||
raise self.warning('circular inclusion in "%s" directive:\n%s'
|
||||
% (self.name, inclusion_chain))
|
||||
|
||||
if 'parser' in self.options:
|
||||
# parse into a dummy document and return created nodes
|
||||
document = utils.new_document(path, settings)
|
||||
document.include_log = include_log + [(path, clip_options)]
|
||||
parser = self.options['parser']()
|
||||
parser.parse('\n'.join(include_lines), document)
|
||||
# clean up doctree and complete parsing
|
||||
document.transformer.populate_from_components((parser,))
|
||||
document.transformer.apply_transforms()
|
||||
return document.children
|
||||
|
||||
# Include as rST source:
|
||||
#
|
||||
# mark end (cf. parsers.rst.states.Body.comment())
|
||||
include_lines += ['', '.. end of inclusion from "%s"' % path]
|
||||
self.state_machine.insert_input(include_lines, path)
|
||||
# update include-log
|
||||
include_log.append((path, clip_options))
|
||||
return []
|
||||
|
||||
|
||||
class Raw(Directive):
|
||||
|
||||
"""
|
||||
Pass through content unchanged
|
||||
|
||||
Content is included in output based on type argument
|
||||
|
||||
Content may be included inline (content section of directive) or
|
||||
imported from a file or url.
|
||||
"""
|
||||
|
||||
required_arguments = 1
|
||||
optional_arguments = 0
|
||||
final_argument_whitespace = True
|
||||
option_spec = {'file': directives.path,
|
||||
'url': directives.uri,
|
||||
'encoding': directives.encoding,
|
||||
'class': directives.class_option}
|
||||
has_content = True
|
||||
|
||||
def run(self):
|
||||
settings = self.state.document.settings
|
||||
if (not settings.raw_enabled
|
||||
or (not settings.file_insertion_enabled
|
||||
and ('file' in self.options or 'url' in self.options))):
|
||||
raise self.warning('"%s" directive disabled.' % self.name)
|
||||
attributes = {'format': ' '.join(self.arguments[0].lower().split())}
|
||||
encoding = self.options.get('encoding', settings.input_encoding)
|
||||
error_handler = settings.input_encoding_error_handler
|
||||
if self.content:
|
||||
if 'file' in self.options or 'url' in self.options:
|
||||
raise self.error(
|
||||
'"%s" directive may not both specify an external file '
|
||||
'and have content.' % self.name)
|
||||
text = '\n'.join(self.content)
|
||||
elif 'file' in self.options:
|
||||
if 'url' in self.options:
|
||||
raise self.error(
|
||||
'The "file" and "url" options may not be simultaneously '
|
||||
'specified for the "%s" directive.' % self.name)
|
||||
path = adapt_path(self.options['file'],
|
||||
self.state.document.current_source,
|
||||
settings.root_prefix)
|
||||
try:
|
||||
raw_file = io.FileInput(source_path=path,
|
||||
encoding=encoding,
|
||||
error_handler=error_handler)
|
||||
except OSError as error:
|
||||
raise self.severe(f'Problems with "{self.name}" directive '
|
||||
f'path:\n{io.error_string(error)}.')
|
||||
else:
|
||||
# TODO: currently, raw input files are recorded as
|
||||
# dependencies even if not used for the chosen output format.
|
||||
settings.record_dependencies.add(path)
|
||||
try:
|
||||
text = raw_file.read()
|
||||
except UnicodeError as error:
|
||||
raise self.severe(f'Problem with "{self.name}" directive:\n'
|
||||
+ io.error_string(error))
|
||||
attributes['source'] = path
|
||||
elif 'url' in self.options:
|
||||
source = self.options['url']
|
||||
try:
|
||||
raw_text = urlopen(source).read()
|
||||
except (URLError, OSError) as error:
|
||||
raise self.severe(f'Problems with "{self.name}" directive URL '
|
||||
f'"{self.options["url"]}":\n'
|
||||
f'{io.error_string(error)}.')
|
||||
raw_file = io.StringInput(source=raw_text, source_path=source,
|
||||
encoding=encoding,
|
||||
error_handler=error_handler)
|
||||
try:
|
||||
text = raw_file.read()
|
||||
except UnicodeError as error:
|
||||
raise self.severe(f'Problem with "{self.name}" directive:\n'
|
||||
+ io.error_string(error))
|
||||
attributes['source'] = source
|
||||
else:
|
||||
# This will always fail because there is no content.
|
||||
self.assert_has_content()
|
||||
raw_node = nodes.raw('', text, classes=self.options.get('class', []),
|
||||
**attributes)
|
||||
(raw_node.source,
|
||||
raw_node.line) = self.state_machine.get_source_and_line(self.lineno)
|
||||
return [raw_node]
|
||||
|
||||
|
||||
class Replace(Directive):
|
||||
|
||||
has_content = True
|
||||
|
||||
def run(self):
|
||||
if not isinstance(self.state, states.SubstitutionDef):
|
||||
raise self.error(
|
||||
'Invalid context: the "%s" directive can only be used within '
|
||||
'a substitution definition.' % self.name)
|
||||
self.assert_has_content()
|
||||
text = '\n'.join(self.content)
|
||||
element = nodes.Element(text)
|
||||
self.state.nested_parse(self.content, self.content_offset,
|
||||
element)
|
||||
# element might contain [paragraph] + system_message(s)
|
||||
node = None
|
||||
messages = []
|
||||
for elem in element:
|
||||
if not node and isinstance(elem, nodes.paragraph):
|
||||
node = elem
|
||||
elif isinstance(elem, nodes.system_message):
|
||||
elem['backrefs'] = []
|
||||
messages.append(elem)
|
||||
else:
|
||||
return [
|
||||
self.reporter.error(
|
||||
f'Error in "{self.name}" directive: may contain '
|
||||
'a single paragraph only.', line=self.lineno)]
|
||||
if node:
|
||||
return messages + node.children
|
||||
return messages
|
||||
|
||||
|
||||
class Unicode(Directive):
|
||||
|
||||
r"""
|
||||
Convert Unicode character codes (numbers) to characters. Codes may be
|
||||
decimal numbers, hexadecimal numbers (prefixed by ``0x``, ``x``, ``\x``,
|
||||
``U+``, ``u``, or ``\u``; e.g. ``U+262E``), or XML-style numeric character
|
||||
entities (e.g. ``☮``). Text following ".." is a comment and is
|
||||
ignored. Spaces are ignored, and any other text remains as-is.
|
||||
"""
|
||||
|
||||
required_arguments = 1
|
||||
optional_arguments = 0
|
||||
final_argument_whitespace = True
|
||||
option_spec = {'trim': directives.flag,
|
||||
'ltrim': directives.flag,
|
||||
'rtrim': directives.flag}
|
||||
|
||||
comment_pattern = re.compile(r'( |\n|^)\.\. ')
|
||||
|
||||
def run(self):
|
||||
if not isinstance(self.state, states.SubstitutionDef):
|
||||
raise self.error(
|
||||
'Invalid context: the "%s" directive can only be used within '
|
||||
'a substitution definition.' % self.name)
|
||||
substitution_definition = self.state_machine.node
|
||||
if 'trim' in self.options:
|
||||
substitution_definition.attributes['ltrim'] = 1
|
||||
substitution_definition.attributes['rtrim'] = 1
|
||||
if 'ltrim' in self.options:
|
||||
substitution_definition.attributes['ltrim'] = 1
|
||||
if 'rtrim' in self.options:
|
||||
substitution_definition.attributes['rtrim'] = 1
|
||||
codes = self.comment_pattern.split(self.arguments[0])[0].split()
|
||||
element = nodes.Element()
|
||||
for code in codes:
|
||||
try:
|
||||
decoded = directives.unicode_code(code)
|
||||
except ValueError as error:
|
||||
raise self.error('Invalid character code: %s\n%s'
|
||||
% (code, io.error_string(error)))
|
||||
element += nodes.Text(decoded)
|
||||
return element.children
|
||||
|
||||
|
||||
class Class(Directive):
|
||||
|
||||
"""
|
||||
Set a "class" attribute on the directive content or the next element.
|
||||
When applied to the next element, a "pending" element is inserted, and a
|
||||
transform does the work later.
|
||||
"""
|
||||
|
||||
required_arguments = 1
|
||||
optional_arguments = 0
|
||||
final_argument_whitespace = True
|
||||
has_content = True
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
class_value = directives.class_option(self.arguments[0])
|
||||
except ValueError:
|
||||
raise self.error(
|
||||
'Invalid class attribute value for "%s" directive: "%s".'
|
||||
% (self.name, self.arguments[0]))
|
||||
node_list = []
|
||||
if self.content:
|
||||
container = nodes.Element()
|
||||
self.state.nested_parse(self.content, self.content_offset,
|
||||
container)
|
||||
for node in container:
|
||||
node['classes'].extend(class_value)
|
||||
node_list.extend(container.children)
|
||||
else:
|
||||
pending = nodes.pending(
|
||||
misc.ClassAttribute,
|
||||
{'class': class_value, 'directive': self.name},
|
||||
self.block_text)
|
||||
self.state_machine.document.note_pending(pending)
|
||||
node_list.append(pending)
|
||||
return node_list
|
||||
|
||||
|
||||
class Role(Directive):
|
||||
|
||||
has_content = True
|
||||
|
||||
argument_pattern = re.compile(r'(%s)\s*(\(\s*(%s)\s*\)\s*)?$'
|
||||
% ((states.Inliner.simplename,) * 2))
|
||||
|
||||
def run(self):
|
||||
"""Dynamically create and register a custom interpreted text role."""
|
||||
if self.content_offset > self.lineno or not self.content:
|
||||
raise self.error('"%s" directive requires arguments on the first '
|
||||
'line.' % self.name)
|
||||
args = self.content[0]
|
||||
match = self.argument_pattern.match(args)
|
||||
if not match:
|
||||
raise self.error('"%s" directive arguments not valid role names: '
|
||||
'"%s".' % (self.name, args))
|
||||
new_role_name = match.group(1)
|
||||
base_role_name = match.group(3)
|
||||
messages = []
|
||||
if base_role_name:
|
||||
base_role, messages = roles.role(
|
||||
base_role_name, self.state_machine.language, self.lineno,
|
||||
self.state.reporter)
|
||||
if base_role is None:
|
||||
error = self.state.reporter.error(
|
||||
'Unknown interpreted text role "%s".' % base_role_name,
|
||||
nodes.literal_block(self.block_text, self.block_text),
|
||||
line=self.lineno)
|
||||
return messages + [error]
|
||||
else:
|
||||
base_role = roles.generic_custom_role
|
||||
assert not hasattr(base_role, 'arguments'), (
|
||||
'Supplemental directive arguments for "%s" directive not '
|
||||
'supported (specified by "%r" role).' % (self.name, base_role))
|
||||
try:
|
||||
converted_role = convert_directive_function(base_role)
|
||||
(arguments, options, content, content_offset
|
||||
) = self.state.parse_directive_block(
|
||||
self.content[1:], self.content_offset,
|
||||
converted_role, option_presets={})
|
||||
except states.MarkupError as detail:
|
||||
error = self.reporter.error(
|
||||
'Error in "%s" directive:\n%s.' % (self.name, detail),
|
||||
nodes.literal_block(self.block_text, self.block_text),
|
||||
line=self.lineno)
|
||||
return messages + [error]
|
||||
if 'class' not in options:
|
||||
try:
|
||||
options['class'] = directives.class_option(new_role_name)
|
||||
except ValueError as detail:
|
||||
error = self.reporter.error(
|
||||
'Invalid argument for "%s" directive:\n%s.'
|
||||
% (self.name, detail),
|
||||
nodes.literal_block(self.block_text, self.block_text),
|
||||
line=self.lineno)
|
||||
return messages + [error]
|
||||
role = roles.CustomRole(new_role_name, base_role, options, content)
|
||||
roles.register_local_role(new_role_name, role)
|
||||
return messages
|
||||
|
||||
|
||||
class DefaultRole(Directive):
|
||||
|
||||
"""Set the default interpreted text role."""
|
||||
|
||||
optional_arguments = 1
|
||||
final_argument_whitespace = False
|
||||
|
||||
def run(self):
|
||||
if not self.arguments:
|
||||
if '' in roles._roles:
|
||||
# restore the "default" default role
|
||||
del roles._roles['']
|
||||
return []
|
||||
role_name = self.arguments[0]
|
||||
role, messages = roles.role(role_name, self.state_machine.language,
|
||||
self.lineno, self.state.reporter)
|
||||
if role is None:
|
||||
error = self.state.reporter.error(
|
||||
'Unknown interpreted text role "%s".' % role_name,
|
||||
nodes.literal_block(self.block_text, self.block_text),
|
||||
line=self.lineno)
|
||||
return messages + [error]
|
||||
roles._roles[''] = role
|
||||
return messages
|
||||
|
||||
|
||||
class Title(Directive):
|
||||
|
||||
required_arguments = 1
|
||||
optional_arguments = 0
|
||||
final_argument_whitespace = True
|
||||
|
||||
def run(self):
|
||||
self.state_machine.document['title'] = self.arguments[0]
|
||||
return []
|
||||
|
||||
|
||||
class MetaBody(states.SpecializedBody):
|
||||
|
||||
def field_marker(self, match, context, next_state):
|
||||
"""Meta element."""
|
||||
node, blank_finish = self.parsemeta(match)
|
||||
self.parent += node
|
||||
return [], next_state, []
|
||||
|
||||
def parsemeta(self, match):
|
||||
name = self.parse_field_marker(match)
|
||||
name = nodes.unescape(utils.escape2null(name))
|
||||
(indented, indent, line_offset, blank_finish
|
||||
) = self.state_machine.get_first_known_indented(match.end())
|
||||
node = nodes.meta()
|
||||
node['content'] = nodes.unescape(utils.escape2null(
|
||||
' '.join(indented)))
|
||||
if not indented:
|
||||
line = self.state_machine.line
|
||||
msg = self.reporter.info(
|
||||
'No content for meta tag "%s".' % name,
|
||||
nodes.literal_block(line, line))
|
||||
return msg, blank_finish
|
||||
tokens = name.split()
|
||||
try:
|
||||
attname, val = utils.extract_name_value(tokens[0])[0]
|
||||
node[attname.lower()] = val
|
||||
except utils.NameValueError:
|
||||
node['name'] = tokens[0]
|
||||
for token in tokens[1:]:
|
||||
try:
|
||||
attname, val = utils.extract_name_value(token)[0]
|
||||
node[attname.lower()] = val
|
||||
except utils.NameValueError as detail:
|
||||
line = self.state_machine.line
|
||||
msg = self.reporter.error(
|
||||
'Error parsing meta tag attribute "%s": %s.'
|
||||
% (token, detail), nodes.literal_block(line, line))
|
||||
return msg, blank_finish
|
||||
return node, blank_finish
|
||||
|
||||
|
||||
class Meta(Directive):
|
||||
|
||||
has_content = True
|
||||
|
||||
SMkwargs = {'state_classes': (MetaBody,)}
|
||||
|
||||
def run(self):
|
||||
self.assert_has_content()
|
||||
node = nodes.Element()
|
||||
new_line_offset, blank_finish = self.state.nested_list_parse(
|
||||
self.content, self.content_offset, node,
|
||||
initial_state='MetaBody', blank_finish=True,
|
||||
state_machine_kwargs=self.SMkwargs)
|
||||
if (new_line_offset - self.content_offset) != len(self.content):
|
||||
# incomplete parse of block?
|
||||
error = self.reporter.error(
|
||||
'Invalid meta directive.',
|
||||
nodes.literal_block(self.block_text, self.block_text),
|
||||
line=self.lineno)
|
||||
node += error
|
||||
# insert at begin of document
|
||||
index = self.state.document.first_child_not_matching_class(
|
||||
(nodes.Titular, nodes.meta)) or 0
|
||||
self.state.document[index:index] = node.children
|
||||
return []
|
||||
|
||||
|
||||
class Date(Directive):
|
||||
|
||||
has_content = True
|
||||
|
||||
def run(self):
|
||||
if not isinstance(self.state, states.SubstitutionDef):
|
||||
raise self.error(
|
||||
'Invalid context: the "%s" directive can only be used within '
|
||||
'a substitution definition.' % self.name)
|
||||
format_str = '\n'.join(self.content) or '%Y-%m-%d'
|
||||
# @@@
|
||||
# Use timestamp from the `SOURCE_DATE_EPOCH`_ environment variable?
|
||||
# Pro: Docutils-generated documentation
|
||||
# can easily be part of `reproducible software builds`__
|
||||
#
|
||||
# __ https://reproducible-builds.org/
|
||||
#
|
||||
# Con: Changes the specs, hard to predict behaviour,
|
||||
#
|
||||
# See also the discussion about \date \time \year in TeX
|
||||
# http://tug.org/pipermail/tex-k/2016-May/002704.html
|
||||
# source_date_epoch = os.environ.get('SOURCE_DATE_EPOCH')
|
||||
# if (source_date_epoch):
|
||||
# text = time.strftime(format_str,
|
||||
# time.gmtime(int(source_date_epoch)))
|
||||
# else:
|
||||
text = time.strftime(format_str)
|
||||
return [nodes.Text(text)]
|
||||
|
||||
|
||||
class TestDirective(Directive):
|
||||
|
||||
"""This directive is useful only for testing purposes."""
|
||||
|
||||
optional_arguments = 1
|
||||
final_argument_whitespace = True
|
||||
option_spec = {'option': directives.unchanged_required}
|
||||
has_content = True
|
||||
|
||||
def run(self):
|
||||
if self.content:
|
||||
text = '\n'.join(self.content)
|
||||
info = self.reporter.info(
|
||||
'Directive processed. Type="%s", arguments=%r, options=%r, '
|
||||
'content:' % (self.name, self.arguments, self.options),
|
||||
nodes.literal_block(text, text), line=self.lineno)
|
||||
else:
|
||||
info = self.reporter.info(
|
||||
'Directive processed. Type="%s", arguments=%r, options=%r, '
|
||||
'content: None' % (self.name, self.arguments, self.options),
|
||||
line=self.lineno)
|
||||
return [info]
|
|
@ -0,0 +1,126 @@
|
|||
# $Id: parts.py 8993 2022-01-29 13:20:04Z milde $
|
||||
# Authors: David Goodger <goodger@python.org>; Dmitry Jemerov
|
||||
# Copyright: This module has been placed in the public domain.
|
||||
|
||||
"""
|
||||
Directives for document parts.
|
||||
"""
|
||||
|
||||
__docformat__ = 'reStructuredText'
|
||||
|
||||
from docutils import nodes, languages
|
||||
from docutils.transforms import parts
|
||||
from docutils.parsers.rst import Directive
|
||||
from docutils.parsers.rst import directives
|
||||
|
||||
|
||||
class Contents(Directive):
|
||||
|
||||
"""
|
||||
Table of contents.
|
||||
|
||||
The table of contents is generated in two passes: initial parse and
|
||||
transform. During the initial parse, a 'pending' element is generated
|
||||
which acts as a placeholder, storing the TOC title and any options
|
||||
internally. At a later stage in the processing, the 'pending' element is
|
||||
replaced by a 'topic' element, a title and the table of contents proper.
|
||||
"""
|
||||
|
||||
backlinks_values = ('top', 'entry', 'none')
|
||||
|
||||
def backlinks(arg):
|
||||
value = directives.choice(arg, Contents.backlinks_values)
|
||||
if value == 'none':
|
||||
return None
|
||||
else:
|
||||
return value
|
||||
|
||||
optional_arguments = 1
|
||||
final_argument_whitespace = True
|
||||
option_spec = {'depth': directives.nonnegative_int,
|
||||
'local': directives.flag,
|
||||
'backlinks': backlinks,
|
||||
'class': directives.class_option}
|
||||
|
||||
def run(self):
|
||||
if not (self.state_machine.match_titles
|
||||
or isinstance(self.state_machine.node, nodes.sidebar)):
|
||||
raise self.error('The "%s" directive may not be used within '
|
||||
'topics or body elements.' % self.name)
|
||||
document = self.state_machine.document
|
||||
language = languages.get_language(document.settings.language_code,
|
||||
document.reporter)
|
||||
if self.arguments:
|
||||
title_text = self.arguments[0]
|
||||
text_nodes, messages = self.state.inline_text(title_text,
|
||||
self.lineno)
|
||||
title = nodes.title(title_text, '', *text_nodes)
|
||||
else:
|
||||
messages = []
|
||||
if 'local' in self.options:
|
||||
title = None
|
||||
else:
|
||||
title = nodes.title('', language.labels['contents'])
|
||||
topic = nodes.topic(classes=['contents'])
|
||||
topic['classes'] += self.options.get('class', [])
|
||||
# the latex2e writer needs source and line for a warning:
|
||||
topic.source, topic.line = self.state_machine.get_source_and_line()
|
||||
topic.line -= 1
|
||||
if 'local' in self.options:
|
||||
topic['classes'].append('local')
|
||||
if title:
|
||||
name = title.astext()
|
||||
topic += title
|
||||
else:
|
||||
name = language.labels['contents']
|
||||
name = nodes.fully_normalize_name(name)
|
||||
if not document.has_name(name):
|
||||
topic['names'].append(name)
|
||||
document.note_implicit_target(topic)
|
||||
pending = nodes.pending(parts.Contents, rawsource=self.block_text)
|
||||
pending.details.update(self.options)
|
||||
document.note_pending(pending)
|
||||
topic += pending
|
||||
return [topic] + messages
|
||||
|
||||
|
||||
class Sectnum(Directive):
|
||||
|
||||
"""Automatic section numbering."""
|
||||
|
||||
option_spec = {'depth': int,
|
||||
'start': int,
|
||||
'prefix': directives.unchanged_required,
|
||||
'suffix': directives.unchanged_required}
|
||||
|
||||
def run(self):
|
||||
pending = nodes.pending(parts.SectNum)
|
||||
pending.details.update(self.options)
|
||||
self.state_machine.document.note_pending(pending)
|
||||
return [pending]
|
||||
|
||||
|
||||
class Header(Directive):
|
||||
|
||||
"""Contents of document header."""
|
||||
|
||||
has_content = True
|
||||
|
||||
def run(self):
|
||||
self.assert_has_content()
|
||||
header = self.state_machine.document.get_decoration().get_header()
|
||||
self.state.nested_parse(self.content, self.content_offset, header)
|
||||
return []
|
||||
|
||||
|
||||
class Footer(Directive):
|
||||
|
||||
"""Contents of document footer."""
|
||||
|
||||
has_content = True
|
||||
|
||||
def run(self):
|
||||
self.assert_has_content()
|
||||
footer = self.state_machine.document.get_decoration().get_footer()
|
||||
self.state.nested_parse(self.content, self.content_offset, footer)
|
||||
return []
|
|
@ -0,0 +1,29 @@
|
|||
# $Id: references.py 7062 2011-06-30 22:14:29Z milde $
|
||||
# Authors: David Goodger <goodger@python.org>; Dmitry Jemerov
|
||||
# Copyright: This module has been placed in the public domain.
|
||||
|
||||
"""
|
||||
Directives for references and targets.
|
||||
"""
|
||||
|
||||
__docformat__ = 'reStructuredText'
|
||||
|
||||
from docutils import nodes
|
||||
from docutils.transforms import references
|
||||
from docutils.parsers.rst import Directive
|
||||
from docutils.parsers.rst import directives
|
||||
|
||||
|
||||
class TargetNotes(Directive):
|
||||
|
||||
"""Target footnote generation."""
|
||||
|
||||
option_spec = {'class': directives.class_option,
|
||||
'name': directives.unchanged}
|
||||
|
||||
def run(self):
|
||||
pending = nodes.pending(references.TargetNotes)
|
||||
self.add_name(pending)
|
||||
pending.details.update(self.options)
|
||||
self.state_machine.document.note_pending(pending)
|
||||
return [pending]
|
|
@ -0,0 +1,538 @@
|
|||
# $Id: tables.py 9492 2023-11-29 16:58:13Z milde $
|
||||
# Authors: David Goodger <goodger@python.org>; David Priest
|
||||
# Copyright: This module has been placed in the public domain.
|
||||
|
||||
"""
|
||||
Directives for table elements.
|
||||
"""
|
||||
|
||||
__docformat__ = 'reStructuredText'
|
||||
|
||||
|
||||
import csv
|
||||
from urllib.request import urlopen
|
||||
from urllib.error import URLError
|
||||
import warnings
|
||||
|
||||
from docutils import nodes, statemachine
|
||||
from docutils.io import FileInput, StringInput
|
||||
from docutils.parsers.rst import Directive
|
||||
from docutils.parsers.rst import directives
|
||||
from docutils.parsers.rst.directives.misc import adapt_path
|
||||
from docutils.utils import SystemMessagePropagation
|
||||
|
||||
|
||||
def align(argument):
|
||||
return directives.choice(argument, ('left', 'center', 'right'))
|
||||
|
||||
|
||||
class Table(Directive):
|
||||
|
||||
"""
|
||||
Generic table base class.
|
||||
"""
|
||||
|
||||
optional_arguments = 1
|
||||
final_argument_whitespace = True
|
||||
option_spec = {'class': directives.class_option,
|
||||
'name': directives.unchanged,
|
||||
'align': align,
|
||||
'width': directives.length_or_percentage_or_unitless,
|
||||
'widths': directives.value_or(('auto', 'grid'),
|
||||
directives.positive_int_list)}
|
||||
has_content = True
|
||||
|
||||
def make_title(self):
|
||||
if self.arguments:
|
||||
title_text = self.arguments[0]
|
||||
text_nodes, messages = self.state.inline_text(title_text,
|
||||
self.lineno)
|
||||
title = nodes.title(title_text, '', *text_nodes)
|
||||
(title.source,
|
||||
title.line) = self.state_machine.get_source_and_line(self.lineno)
|
||||
else:
|
||||
title = None
|
||||
messages = []
|
||||
return title, messages
|
||||
|
||||
def check_table_dimensions(self, rows, header_rows, stub_columns):
|
||||
if len(rows) < header_rows:
|
||||
error = self.reporter.error('%s header row(s) specified but '
|
||||
'only %s row(s) of data supplied ("%s" directive).'
|
||||
% (header_rows, len(rows), self.name),
|
||||
nodes.literal_block(self.block_text, self.block_text),
|
||||
line=self.lineno)
|
||||
raise SystemMessagePropagation(error)
|
||||
if len(rows) == header_rows > 0:
|
||||
error = self.reporter.error(
|
||||
f'Insufficient data supplied ({len(rows)} row(s)); '
|
||||
'no data remaining for table body, '
|
||||
f'required by "{self.name}" directive.',
|
||||
nodes.literal_block(self.block_text, self.block_text),
|
||||
line=self.lineno)
|
||||
raise SystemMessagePropagation(error)
|
||||
for row in rows:
|
||||
if len(row) < stub_columns:
|
||||
error = self.reporter.error(
|
||||
f'{stub_columns} stub column(s) specified '
|
||||
f'but only {len(row)} columns(s) of data supplied '
|
||||
f'("{self.name}" directive).',
|
||||
nodes.literal_block(self.block_text, self.block_text),
|
||||
line=self.lineno)
|
||||
raise SystemMessagePropagation(error)
|
||||
if len(row) == stub_columns > 0:
|
||||
error = self.reporter.error(
|
||||
'Insufficient data supplied (%s columns(s)); '
|
||||
'no data remaining for table body, required '
|
||||
'by "%s" directive.' % (len(row), self.name),
|
||||
nodes.literal_block(self.block_text, self.block_text),
|
||||
line=self.lineno)
|
||||
raise SystemMessagePropagation(error)
|
||||
|
||||
def set_table_width(self, table_node):
|
||||
if 'width' in self.options:
|
||||
table_node['width'] = self.options.get('width')
|
||||
|
||||
@property
|
||||
def widths(self):
|
||||
return self.options.get('widths', '')
|
||||
|
||||
def get_column_widths(self, n_cols):
|
||||
if isinstance(self.widths, list):
|
||||
if len(self.widths) != n_cols:
|
||||
# TODO: use last value for missing columns?
|
||||
error = self.reporter.error('"%s" widths do not match the '
|
||||
'number of columns in table (%s).' % (self.name, n_cols),
|
||||
nodes.literal_block(self.block_text, self.block_text),
|
||||
line=self.lineno)
|
||||
raise SystemMessagePropagation(error)
|
||||
col_widths = self.widths
|
||||
elif n_cols:
|
||||
col_widths = [100 // n_cols] * n_cols
|
||||
else:
|
||||
error = self.reporter.error('No table data detected in CSV file.',
|
||||
nodes.literal_block(self.block_text, self.block_text),
|
||||
line=self.lineno)
|
||||
raise SystemMessagePropagation(error)
|
||||
return col_widths
|
||||
|
||||
def extend_short_rows_with_empty_cells(self, columns, parts):
|
||||
for part in parts:
|
||||
for row in part:
|
||||
if len(row) < columns:
|
||||
row.extend([(0, 0, 0, [])] * (columns - len(row)))
|
||||
|
||||
|
||||
class RSTTable(Table):
|
||||
"""
|
||||
Class for the `"table" directive`__ for formal tables using rST syntax.
|
||||
|
||||
__ https://docutils.sourceforge.io/docs/ref/rst/directives.html
|
||||
"""
|
||||
|
||||
def run(self):
|
||||
if not self.content:
|
||||
warning = self.reporter.warning('Content block expected '
|
||||
'for the "%s" directive; none found.' % self.name,
|
||||
nodes.literal_block(self.block_text, self.block_text),
|
||||
line=self.lineno)
|
||||
return [warning]
|
||||
title, messages = self.make_title()
|
||||
node = nodes.Element() # anonymous container for parsing
|
||||
self.state.nested_parse(self.content, self.content_offset, node)
|
||||
if len(node) != 1 or not isinstance(node[0], nodes.table):
|
||||
error = self.reporter.error('Error parsing content block for the '
|
||||
'"%s" directive: exactly one table expected.' % self.name,
|
||||
nodes.literal_block(self.block_text, self.block_text),
|
||||
line=self.lineno)
|
||||
return [error]
|
||||
table_node = node[0]
|
||||
table_node['classes'] += self.options.get('class', [])
|
||||
self.set_table_width(table_node)
|
||||
if 'align' in self.options:
|
||||
table_node['align'] = self.options.get('align')
|
||||
if isinstance(self.widths, list):
|
||||
tgroup = table_node[0]
|
||||
try:
|
||||
col_widths = self.get_column_widths(tgroup["cols"])
|
||||
except SystemMessagePropagation as detail:
|
||||
return [detail.args[0]]
|
||||
colspecs = [child for child in tgroup.children
|
||||
if child.tagname == 'colspec']
|
||||
for colspec, col_width in zip(colspecs, col_widths):
|
||||
colspec['colwidth'] = col_width
|
||||
if self.widths == 'auto':
|
||||
table_node['classes'] += ['colwidths-auto']
|
||||
elif self.widths: # "grid" or list of integers
|
||||
table_node['classes'] += ['colwidths-given']
|
||||
self.add_name(table_node)
|
||||
if title:
|
||||
table_node.insert(0, title)
|
||||
return [table_node] + messages
|
||||
|
||||
|
||||
class CSVTable(Table):
|
||||
|
||||
option_spec = {'header-rows': directives.nonnegative_int,
|
||||
'stub-columns': directives.nonnegative_int,
|
||||
'header': directives.unchanged,
|
||||
'width': directives.length_or_percentage_or_unitless,
|
||||
'widths': directives.value_or(('auto', ),
|
||||
directives.positive_int_list),
|
||||
'file': directives.path,
|
||||
'url': directives.uri,
|
||||
'encoding': directives.encoding,
|
||||
'class': directives.class_option,
|
||||
'name': directives.unchanged,
|
||||
'align': align,
|
||||
# field delimiter char
|
||||
'delim': directives.single_char_or_whitespace_or_unicode,
|
||||
# treat whitespace after delimiter as significant
|
||||
'keepspace': directives.flag,
|
||||
# text field quote/unquote char:
|
||||
'quote': directives.single_char_or_unicode,
|
||||
# char used to escape delim & quote as-needed:
|
||||
'escape': directives.single_char_or_unicode}
|
||||
|
||||
class DocutilsDialect(csv.Dialect):
|
||||
|
||||
"""CSV dialect for `csv_table` directive."""
|
||||
|
||||
delimiter = ','
|
||||
quotechar = '"'
|
||||
doublequote = True
|
||||
skipinitialspace = True
|
||||
strict = True
|
||||
lineterminator = '\n'
|
||||
quoting = csv.QUOTE_MINIMAL
|
||||
|
||||
def __init__(self, options):
|
||||
if 'delim' in options:
|
||||
self.delimiter = options['delim']
|
||||
if 'keepspace' in options:
|
||||
self.skipinitialspace = False
|
||||
if 'quote' in options:
|
||||
self.quotechar = options['quote']
|
||||
if 'escape' in options:
|
||||
self.doublequote = False
|
||||
self.escapechar = options['escape']
|
||||
super().__init__()
|
||||
|
||||
class HeaderDialect(csv.Dialect):
|
||||
"""
|
||||
CSV dialect used for the "header" option data.
|
||||
|
||||
Deprecated. Will be removed in Docutils 0.22.
|
||||
"""
|
||||
# The separate HeaderDialect was introduced in revision 2294
|
||||
# (2004-06-17) in the sandbox before the "csv-table" directive moved
|
||||
# to the trunk in r2309. Discussion in docutils-devel around this time
|
||||
# did not mention a rationale (part of the discussion was in private
|
||||
# mail).
|
||||
# This is in conflict with the documentation, which always said:
|
||||
# "Must use the same CSV format as the main CSV data."
|
||||
# and did not change in this aspect.
|
||||
#
|
||||
# Maybe it was intended to have similar escape rules for rST and CSV,
|
||||
# however with the current implementation this means we need
|
||||
# `\\` for rST markup and ``\\\\`` for a literal backslash
|
||||
# in the "option" header but ``\`` and ``\\`` in the header-lines and
|
||||
# table cells of the main CSV data.
|
||||
delimiter = ','
|
||||
quotechar = '"'
|
||||
escapechar = '\\'
|
||||
doublequote = False
|
||||
skipinitialspace = True
|
||||
strict = True
|
||||
lineterminator = '\n'
|
||||
quoting = csv.QUOTE_MINIMAL
|
||||
|
||||
def __init__(self):
|
||||
warnings.warn('CSVTable.HeaderDialect will be removed '
|
||||
'in Docutils 0.22.',
|
||||
PendingDeprecationWarning, stacklevel=2)
|
||||
super().__init__()
|
||||
|
||||
@staticmethod
|
||||
def check_requirements():
|
||||
warnings.warn('CSVTable.check_requirements()'
|
||||
' is not required with Python 3'
|
||||
' and will be removed in Docutils 0.22.',
|
||||
DeprecationWarning, stacklevel=2)
|
||||
|
||||
def process_header_option(self):
|
||||
source = self.state_machine.get_source(self.lineno - 1)
|
||||
table_head = []
|
||||
max_header_cols = 0
|
||||
if 'header' in self.options: # separate table header in option
|
||||
rows, max_header_cols = self.parse_csv_data_into_rows(
|
||||
self.options['header'].split('\n'),
|
||||
self.DocutilsDialect(self.options),
|
||||
source)
|
||||
table_head.extend(rows)
|
||||
return table_head, max_header_cols
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
if (not self.state.document.settings.file_insertion_enabled
|
||||
and ('file' in self.options
|
||||
or 'url' in self.options)):
|
||||
warning = self.reporter.warning('File and URL access '
|
||||
'deactivated; ignoring "%s" directive.' % self.name,
|
||||
nodes.literal_block(self.block_text, self.block_text),
|
||||
line=self.lineno)
|
||||
return [warning]
|
||||
title, messages = self.make_title()
|
||||
csv_data, source = self.get_csv_data()
|
||||
table_head, max_header_cols = self.process_header_option()
|
||||
rows, max_cols = self.parse_csv_data_into_rows(
|
||||
csv_data, self.DocutilsDialect(self.options), source)
|
||||
max_cols = max(max_cols, max_header_cols)
|
||||
header_rows = self.options.get('header-rows', 0)
|
||||
stub_columns = self.options.get('stub-columns', 0)
|
||||
self.check_table_dimensions(rows, header_rows, stub_columns)
|
||||
table_head.extend(rows[:header_rows])
|
||||
table_body = rows[header_rows:]
|
||||
col_widths = self.get_column_widths(max_cols)
|
||||
self.extend_short_rows_with_empty_cells(max_cols,
|
||||
(table_head, table_body))
|
||||
except SystemMessagePropagation as detail:
|
||||
return [detail.args[0]]
|
||||
except csv.Error as detail:
|
||||
message = str(detail)
|
||||
error = self.reporter.error('Error with CSV data'
|
||||
' in "%s" directive:\n%s' % (self.name, message),
|
||||
nodes.literal_block(self.block_text, self.block_text),
|
||||
line=self.lineno)
|
||||
return [error]
|
||||
table = (col_widths, table_head, table_body)
|
||||
table_node = self.state.build_table(table, self.content_offset,
|
||||
stub_columns, widths=self.widths)
|
||||
table_node['classes'] += self.options.get('class', [])
|
||||
if 'align' in self.options:
|
||||
table_node['align'] = self.options.get('align')
|
||||
self.set_table_width(table_node)
|
||||
self.add_name(table_node)
|
||||
if title:
|
||||
table_node.insert(0, title)
|
||||
return [table_node] + messages
|
||||
|
||||
def get_csv_data(self):
|
||||
"""
|
||||
Get CSV data from the directive content, from an external
|
||||
file, or from a URL reference.
|
||||
"""
|
||||
settings = self.state.document.settings
|
||||
encoding = self.options.get('encoding', settings.input_encoding)
|
||||
error_handler = settings.input_encoding_error_handler
|
||||
if self.content:
|
||||
# CSV data is from directive content.
|
||||
if 'file' in self.options or 'url' in self.options:
|
||||
error = self.reporter.error('"%s" directive may not both '
|
||||
'specify an external file and have content.' % self.name,
|
||||
nodes.literal_block(self.block_text, self.block_text),
|
||||
line=self.lineno)
|
||||
raise SystemMessagePropagation(error)
|
||||
source = self.content.source(0)
|
||||
csv_data = self.content
|
||||
elif 'file' in self.options:
|
||||
# CSV data is from an external file.
|
||||
if 'url' in self.options:
|
||||
error = self.reporter.error('The "file" and "url" options '
|
||||
'may not be simultaneously specified '
|
||||
'for the "%s" directive.' % self.name,
|
||||
nodes.literal_block(self.block_text, self.block_text),
|
||||
line=self.lineno)
|
||||
raise SystemMessagePropagation(error)
|
||||
source = adapt_path(self.options['file'],
|
||||
self.state.document.current_source,
|
||||
settings.root_prefix)
|
||||
try:
|
||||
csv_file = FileInput(source_path=source,
|
||||
encoding=encoding,
|
||||
error_handler=error_handler)
|
||||
csv_data = csv_file.read().splitlines()
|
||||
except OSError as error:
|
||||
severe = self.reporter.severe(
|
||||
'Problems with "%s" directive path:\n%s.'
|
||||
% (self.name, error),
|
||||
nodes.literal_block(self.block_text, self.block_text),
|
||||
line=self.lineno)
|
||||
raise SystemMessagePropagation(severe)
|
||||
else:
|
||||
settings.record_dependencies.add(source)
|
||||
elif 'url' in self.options:
|
||||
source = self.options['url']
|
||||
try:
|
||||
with urlopen(source) as response:
|
||||
csv_text = response.read()
|
||||
except (URLError, OSError, ValueError) as error:
|
||||
severe = self.reporter.severe(
|
||||
'Problems with "%s" directive URL "%s":\n%s.'
|
||||
% (self.name, self.options['url'], error),
|
||||
nodes.literal_block(self.block_text, self.block_text),
|
||||
line=self.lineno)
|
||||
raise SystemMessagePropagation(severe)
|
||||
csv_file = StringInput(source=csv_text, source_path=source,
|
||||
encoding=encoding,
|
||||
error_handler=error_handler)
|
||||
csv_data = csv_file.read().splitlines()
|
||||
else:
|
||||
error = self.reporter.warning(
|
||||
'The "%s" directive requires content; none supplied.'
|
||||
% self.name,
|
||||
nodes.literal_block(self.block_text, self.block_text),
|
||||
line=self.lineno)
|
||||
raise SystemMessagePropagation(error)
|
||||
return csv_data, source
|
||||
|
||||
@staticmethod
|
||||
def decode_from_csv(s):
|
||||
warnings.warn('CSVTable.decode_from_csv()'
|
||||
' is not required with Python 3'
|
||||
' and will be removed in Docutils 0.21 or later.',
|
||||
DeprecationWarning, stacklevel=2)
|
||||
return s
|
||||
|
||||
@staticmethod
|
||||
def encode_for_csv(s):
|
||||
warnings.warn('CSVTable.encode_from_csv()'
|
||||
' is not required with Python 3'
|
||||
' and will be removed in Docutils 0.21 or later.',
|
||||
DeprecationWarning, stacklevel=2)
|
||||
return s
|
||||
|
||||
def parse_csv_data_into_rows(self, csv_data, dialect, source):
|
||||
csv_reader = csv.reader((line + '\n' for line in csv_data),
|
||||
dialect=dialect)
|
||||
rows = []
|
||||
max_cols = 0
|
||||
for row in csv_reader:
|
||||
row_data = []
|
||||
for cell in row:
|
||||
cell_data = (0, 0, 0, statemachine.StringList(
|
||||
cell.splitlines(), source=source))
|
||||
row_data.append(cell_data)
|
||||
rows.append(row_data)
|
||||
max_cols = max(max_cols, len(row))
|
||||
return rows, max_cols
|
||||
|
||||
|
||||
class ListTable(Table):
|
||||
|
||||
"""
|
||||
Implement tables whose data is encoded as a uniform two-level bullet list.
|
||||
For further ideas, see
|
||||
https://docutils.sourceforge.io/docs/dev/rst/alternatives.html#list-driven-tables
|
||||
"""
|
||||
|
||||
option_spec = {'header-rows': directives.nonnegative_int,
|
||||
'stub-columns': directives.nonnegative_int,
|
||||
'width': directives.length_or_percentage_or_unitless,
|
||||
'widths': directives.value_or(('auto', ),
|
||||
directives.positive_int_list),
|
||||
'class': directives.class_option,
|
||||
'name': directives.unchanged,
|
||||
'align': align}
|
||||
|
||||
def run(self):
|
||||
if not self.content:
|
||||
error = self.reporter.error('The "%s" directive is empty; '
|
||||
'content required.' % self.name,
|
||||
nodes.literal_block(self.block_text, self.block_text),
|
||||
line=self.lineno)
|
||||
return [error]
|
||||
title, messages = self.make_title()
|
||||
node = nodes.Element() # anonymous container for parsing
|
||||
self.state.nested_parse(self.content, self.content_offset, node)
|
||||
try:
|
||||
num_cols, col_widths = self.check_list_content(node)
|
||||
table_data = [[item.children for item in row_list[0]]
|
||||
for row_list in node[0]]
|
||||
header_rows = self.options.get('header-rows', 0)
|
||||
stub_columns = self.options.get('stub-columns', 0)
|
||||
self.check_table_dimensions(table_data, header_rows, stub_columns)
|
||||
except SystemMessagePropagation as detail:
|
||||
return [detail.args[0]]
|
||||
table_node = self.build_table_from_list(table_data, col_widths,
|
||||
header_rows, stub_columns)
|
||||
if 'align' in self.options:
|
||||
table_node['align'] = self.options.get('align')
|
||||
table_node['classes'] += self.options.get('class', [])
|
||||
self.set_table_width(table_node)
|
||||
self.add_name(table_node)
|
||||
if title:
|
||||
table_node.insert(0, title)
|
||||
return [table_node] + messages
|
||||
|
||||
def check_list_content(self, node):
|
||||
if len(node) != 1 or not isinstance(node[0], nodes.bullet_list):
|
||||
error = self.reporter.error(
|
||||
'Error parsing content block for the "%s" directive: '
|
||||
'exactly one bullet list expected.' % self.name,
|
||||
nodes.literal_block(self.block_text, self.block_text),
|
||||
line=self.lineno)
|
||||
raise SystemMessagePropagation(error)
|
||||
list_node = node[0]
|
||||
num_cols = 0
|
||||
# Check for a uniform two-level bullet list:
|
||||
for item_index in range(len(list_node)):
|
||||
item = list_node[item_index]
|
||||
if len(item) != 1 or not isinstance(item[0], nodes.bullet_list):
|
||||
error = self.reporter.error(
|
||||
'Error parsing content block for the "%s" directive: '
|
||||
'two-level bullet list expected, but row %s does not '
|
||||
'contain a second-level bullet list.'
|
||||
% (self.name, item_index + 1),
|
||||
nodes.literal_block(self.block_text, self.block_text),
|
||||
line=self.lineno)
|
||||
raise SystemMessagePropagation(error)
|
||||
elif item_index:
|
||||
if len(item[0]) != num_cols:
|
||||
error = self.reporter.error(
|
||||
'Error parsing content block for the "%s" directive: '
|
||||
'uniform two-level bullet list expected, but row %s '
|
||||
'does not contain the same number of items as row 1 '
|
||||
'(%s vs %s).'
|
||||
% (self.name, item_index + 1, len(item[0]), num_cols),
|
||||
nodes.literal_block(self.block_text, self.block_text),
|
||||
line=self.lineno)
|
||||
raise SystemMessagePropagation(error)
|
||||
else:
|
||||
num_cols = len(item[0])
|
||||
col_widths = self.get_column_widths(num_cols)
|
||||
return num_cols, col_widths
|
||||
|
||||
def build_table_from_list(self, table_data,
|
||||
col_widths, header_rows, stub_columns):
|
||||
table = nodes.table()
|
||||
if self.widths == 'auto':
|
||||
table['classes'] += ['colwidths-auto']
|
||||
elif self.widths: # explicitly set column widths
|
||||
table['classes'] += ['colwidths-given']
|
||||
tgroup = nodes.tgroup(cols=len(col_widths))
|
||||
table += tgroup
|
||||
for col_width in col_widths:
|
||||
colspec = nodes.colspec()
|
||||
if col_width is not None:
|
||||
colspec.attributes['colwidth'] = col_width
|
||||
if stub_columns:
|
||||
colspec.attributes['stub'] = 1
|
||||
stub_columns -= 1
|
||||
tgroup += colspec
|
||||
rows = []
|
||||
for row in table_data:
|
||||
row_node = nodes.row()
|
||||
for cell in row:
|
||||
entry = nodes.entry()
|
||||
entry += cell
|
||||
row_node += entry
|
||||
rows.append(row_node)
|
||||
if header_rows:
|
||||
thead = nodes.thead()
|
||||
thead.extend(rows[:header_rows])
|
||||
tgroup += thead
|
||||
tbody = nodes.tbody()
|
||||
tbody.extend(rows[header_rows:])
|
||||
tgroup += tbody
|
||||
return table
|
Loading…
Add table
Add a link
Reference in a new issue