first commit

This commit is contained in:
Yura 2024-09-15 15:12:16 +03:00
commit 417e54da96
5696 changed files with 900003 additions and 0 deletions

View file

@ -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. ``&#x262E;``). 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))

View file

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

View file

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

View file

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

View file

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

View file

@ -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. ``&#x262E;``). 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]

View file

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

View file

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

View file

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