\n')
+ else: # call the parent method
+ _html_base.HTMLTranslator.depart_example(self, node)
+
+ d) Extend one method (call the parent), but don't otherwise use the
+ `self.context` stack::
+
+ def depart_example(self, node):
+ _html_base.HTMLTranslator.depart_example(self, node)
+ if foo:
+ # implementation-specific code
+ # that does not use `self.context`
+ self.body.append('\n')
+
+ This way, changes in stack use will not bite you.
+ """
+
+ doctype = '\n'
+ doctype_mathml = doctype
+
+ head_prefix_template = ('\n\n')
+ content_type = '\n'
+ generator = (
+ f'\n')
+ # `starttag()` arguments for the main document (HTML5 uses )
+ documenttag_args = {'tagname': 'div', 'CLASS': 'document'}
+
+ # Template for the MathJax script in the header:
+ mathjax_script = '\n'
+
+ mathjax_url = 'file:/usr/share/javascript/mathjax/MathJax.js'
+ """
+ URL of the MathJax javascript library.
+
+ The MathJax library ought to be installed on the same
+ server as the rest of the deployed site files and specified
+ in the `math-output` setting appended to "mathjax".
+ See `Docutils Configuration`__.
+
+ __ https://docutils.sourceforge.io/docs/user/config.html#math-output
+
+ The fallback tries a local MathJax installation at
+ ``/usr/share/javascript/mathjax/MathJax.js``.
+ """
+
+ stylesheet_link = '\n'
+ embedded_stylesheet = '\n'
+ words_and_spaces = re.compile(r'[^ \n]+| +|\n')
+ # wrap point inside word:
+ in_word_wrap_point = re.compile(r'.+\W\W.+|[-?].+')
+ lang_attribute = 'lang' # name changes to 'xml:lang' in XHTML 1.1
+
+ special_characters = {ord('&'): '&',
+ ord('<'): '<',
+ ord('"'): '"',
+ ord('>'): '>',
+ ord('@'): '@', # may thwart address harvesters
+ }
+ """Character references for characters with a special meaning in HTML."""
+
+ videotypes = ('video/mp4', 'video/webm', 'video/ogg')
+ """MIME types supported by the HTML5
\n')
+
+ def visit_author(self, node):
+ if not isinstance(node.parent, nodes.authors):
+ self.visit_docinfo_item(node, 'author')
+ self.body.append('
')
+ if isinstance(node.parent, nodes.authors):
+ self.body.append('\n')
+ else:
+ self.depart_docinfo_item()
+
+ def visit_authors(self, node):
+ self.visit_docinfo_item(node, 'authors')
+
+ def depart_authors(self, node):
+ self.depart_docinfo_item()
+
+ def visit_block_quote(self, node):
+ self.body.append(self.starttag(node, 'blockquote'))
+
+ def depart_block_quote(self, node):
+ self.body.append('\n')
+
+ def check_simple_list(self, node):
+ """Check for a simple list that can be rendered compactly."""
+ visitor = SimpleListChecker(self.document)
+ try:
+ node.walk(visitor)
+ except nodes.NodeFound:
+ return False
+ else:
+ return True
+
+ # Compact lists
+ # ------------
+ # Include definition lists and field lists (in addition to ordered
+ # and unordered lists) in the test if a list is "simple" (cf. the
+ # html4css1.HTMLTranslator docstring and the SimpleListChecker class at
+ # the end of this file).
+
+ def is_compactable(self, node):
+ # explicit class arguments have precedence
+ if 'compact' in node['classes']:
+ return True
+ if 'open' in node['classes']:
+ return False
+ # check config setting:
+ if (isinstance(node, (nodes.field_list, nodes.definition_list))
+ and not self.settings.compact_field_lists):
+ return False
+ if (isinstance(node, (nodes.enumerated_list, nodes.bullet_list))
+ and not self.settings.compact_lists):
+ return False
+ # Table of Contents:
+ if 'contents' in node.parent['classes']:
+ return True
+ # check the list items:
+ return self.check_simple_list(node)
+
+ def visit_bullet_list(self, node):
+ atts = {}
+ old_compact_simple = self.compact_simple
+ self.context.append((self.compact_simple, self.compact_p))
+ self.compact_p = None
+ self.compact_simple = self.is_compactable(node)
+ if self.compact_simple and not old_compact_simple:
+ atts['class'] = 'simple'
+ self.body.append(self.starttag(node, 'ul', **atts))
+
+ def depart_bullet_list(self, node):
+ self.compact_simple, self.compact_p = self.context.pop()
+ self.body.append('\n')
+
+ def visit_caption(self, node):
+ self.body.append(self.starttag(node, 'p', '', CLASS='caption'))
+
+ def depart_caption(self, node):
+ self.body.append('\n')
+
+ # Use semantic tag and DPub role (HTML4 uses a table)
+ def visit_citation(self, node):
+ # role 'doc-bibloentry' requires wrapping in an element with
+ # role 'list' and an element with role 'doc-bibliography'
+ # https://www.w3.org/TR/dpub-aria-1.0/#doc-biblioentry)
+ if not isinstance(node.previous_sibling(), type(node)):
+ self.body.append('
\n')
+ if not isinstance(node.next_node(descend=False, siblings=True),
+ type(node)):
+ self.body.append('\n')
+
+ # Use DPub role (overwritten in HTML4)
+ def visit_citation_reference(self, node):
+ href = '#'
+ if 'refid' in node:
+ href += node['refid']
+ elif 'refname' in node:
+ href += self.document.nameids[node['refname']]
+ # else: # TODO system message (or already in the transform)?
+ # 'Citation reference missing.'
+ self.body.append(self.starttag(node, 'a', suffix='[', href=href,
+ classes=['citation-reference'],
+ role='doc-biblioref'))
+
+ def depart_citation_reference(self, node):
+ self.body.append(']')
+
+ def visit_classifier(self, node):
+ self.body.append(self.starttag(node, 'span', '', CLASS='classifier'))
+
+ def depart_classifier(self, node):
+ self.body.append('')
+ self.depart_term(node) # close the term element after last classifier
+
+ def visit_colspec(self, node):
+ self.colspecs.append(node)
+ # "stubs" list is an attribute of the tgroup element:
+ node.parent.stubs.append(node.attributes.get('stub'))
+
+ def depart_colspec(self, node):
+ # write out
when all colspecs are processed
+ if isinstance(node.next_node(descend=False, siblings=True),
+ nodes.colspec):
+ return
+ if 'colwidths-auto' in node.parent.parent['classes'] or (
+ 'colwidths-grid' not in self.settings.table_style
+ and 'colwidths-given' not in node.parent.parent['classes']):
+ return
+ self.body.append(self.starttag(node, 'colgroup'))
+ total_width = sum(node['colwidth'] for node in self.colspecs)
+ for node in self.colspecs:
+ colwidth = node['colwidth'] / total_width
+ self.body.append(self.emptytag(node, 'col',
+ style=f'width: {colwidth:.1%}'))
+ self.body.append('
\n')
+ self.body.append(self.starttag(node, 'dd', '', CLASS=name))
+
+ def depart_docinfo_item(self):
+ self.body.append('\n')
+
+ def visit_doctest_block(self, node):
+ self.body.append(self.starttag(node, 'pre', suffix='',
+ classes=['code', 'python', 'doctest']))
+
+ def depart_doctest_block(self, node):
+ self.body.append('\n\n')
+
+ def visit_document(self, node):
+ title = (node.get('title') or os.path.basename(node['source'])
+ or 'untitled Docutils document')
+ self.head.append(f'{self.encode(title)}\n')
+
+ def depart_document(self, node):
+ self.head_prefix.extend([self.doctype,
+ self.head_prefix_template %
+ {'lang': self.settings.language_code}])
+ self.html_prolog.append(self.doctype)
+ self.head = self.meta[:] + self.head
+ if 'name="dcterms.' in ''.join(self.meta):
+ self.head.append('')
+ if self.math_header:
+ if self.math_output == 'mathjax':
+ self.head.extend(self.math_header)
+ else:
+ self.stylesheet.extend(self.math_header)
+ # skip content-type meta tag with interpolated charset value:
+ self.html_head.extend(self.head[1:])
+ self.body_prefix.append(self.starttag(node, **self.documenttag_args))
+ self.body_suffix.insert(0, f'{self.documenttag_args["tagname"]}>\n')
+ self.fragment.extend(self.body) # self.fragment is the "naked" body
+ self.html_body.extend(self.body_prefix[1:] + self.body_pre_docinfo
+ + self.docinfo + self.body
+ + self.body_suffix[:-1])
+ assert not self.context, f'len(context) = {len(self.context)}'
+
+ def visit_emphasis(self, node):
+ self.body.append(self.starttag(node, 'em', ''))
+
+ def depart_emphasis(self, node):
+ self.body.append('')
+
+ def visit_entry(self, node):
+ atts = {'classes': []}
+ if isinstance(node.parent.parent, nodes.thead):
+ atts['classes'].append('head')
+ if node.parent.parent.parent.stubs[node.parent.column]:
+ # "stubs" list is an attribute of the tgroup element
+ atts['classes'].append('stub')
+ if atts['classes']:
+ tagname = 'th'
+ else:
+ tagname = 'td'
+ node.parent.column += 1
+ if 'morerows' in node:
+ atts['rowspan'] = node['morerows'] + 1
+ if 'morecols' in node:
+ atts['colspan'] = node['morecols'] + 1
+ node.parent.column += node['morecols']
+ self.body.append(self.starttag(node, tagname, '', **atts))
+ self.context.append('%s>\n' % tagname.lower())
+
+ def depart_entry(self, node):
+ self.body.append(self.context.pop())
+
+ def visit_enumerated_list(self, node):
+ atts = {'classes': []}
+ if 'start' in node:
+ atts['start'] = node['start']
+ if 'enumtype' in node:
+ atts['classes'].append(node['enumtype'])
+ if self.is_compactable(node):
+ atts['classes'].append('simple')
+ self.body.append(self.starttag(node, 'ol', **atts))
+
+ def depart_enumerated_list(self, node):
+ self.body.append('\n')
+
+ def visit_field_list(self, node):
+ atts = {}
+ classes = node.setdefault('classes', [])
+ for i, cls in enumerate(classes):
+ if cls.startswith('field-indent-'):
+ try:
+ indent_length = length_or_percentage_or_unitless(
+ cls[13:], 'px')
+ except ValueError:
+ break
+ atts['style'] = '--field-indent: %s;' % indent_length
+ classes.pop(i)
+ break
+ classes.append('field-list')
+ if self.is_compactable(node):
+ classes.append('simple')
+ self.body.append(self.starttag(node, 'dl', **atts))
+
+ def depart_field_list(self, node):
+ self.body.append('\n')
+
+ def visit_field(self, node):
+ # Insert children ( and ) directly.
+ # Transfer "id" attribute to the child node.
+ for child in node:
+ if isinstance(child, nodes.field_name):
+ child['ids'].extend(node['ids'])
+
+ def depart_field(self, node):
+ pass
+
+ # as field is ignored, pass class arguments to field-name and field-body:
+ def visit_field_name(self, node):
+ self.body.append(self.starttag(node, 'dt', '',
+ classes=node.parent['classes']))
+
+ def depart_field_name(self, node):
+ self.body.append(':\n')
+
+ def visit_field_body(self, node):
+ self.body.append(self.starttag(node, 'dd', '',
+ classes=node.parent['classes']))
+ # prevent misalignment of following content if the field is empty:
+ if not node.children:
+ self.body.append('')
+
+ def depart_field_body(self, node):
+ self.body.append('\n')
+
+ def visit_figure(self, node):
+ atts = {'class': 'figure'}
+ if node.get('width'):
+ atts['style'] = 'width: %s' % node['width']
+ if node.get('align'):
+ atts['class'] += " align-" + node['align']
+ self.body.append(self.starttag(node, 'div', **atts))
+
+ def depart_figure(self, node):
+ self.body.append('\n')
+
+ def visit_footer(self, node):
+ self.context.append(len(self.body))
+
+ def depart_footer(self, node):
+ start = self.context.pop()
+ footer = [self.starttag(node, 'div', CLASS='footer'),
+ '\n']
+ footer.extend(self.body[start:])
+ footer.append('\n\n')
+ self.footer.extend(footer)
+ self.body_suffix[:0] = footer
+ del self.body[start:]
+
+ def visit_footnote(self, node):
+ # No native HTML element: use \n')
+
+ def visit_footnote_reference(self, node):
+ href = '#' + node['refid']
+ classes = [self.settings.footnote_references]
+ self.body.append(self.starttag(node, 'a', suffix='', classes=classes,
+ role='doc-noteref', href=href))
+ self.body.append('[')
+
+ def depart_footnote_reference(self, node):
+ self.body.append(']')
+ self.body.append('')
+
+ # Docutils-generated text: put section numbers in a span for CSS styling:
+ def visit_generated(self, node):
+ if 'sectnum' in node['classes']:
+ # get section number (strip trailing no-break-spaces)
+ sectnum = node.astext().rstrip(' ')
+ self.body.append('%s '
+ % self.encode(sectnum))
+ # Content already processed:
+ raise nodes.SkipNode
+
+ def depart_generated(self, node):
+ pass
+
+ def visit_header(self, node):
+ self.context.append(len(self.body))
+
+ def depart_header(self, node):
+ start = self.context.pop()
+ header = [self.starttag(node, 'div', CLASS='header')]
+ header.extend(self.body[start:])
+ header.append('\n\n\n')
+ self.body_prefix.extend(header)
+ self.header.extend(header)
+ del self.body[start:]
+
+ def visit_image(self, node):
+ # reference/embed images (still images and videos)
+ uri = node['uri']
+ alt = node.get('alt', uri)
+ mimetype = mimetypes.guess_type(uri)[0]
+ element = '' # the HTML element (including potential children)
+ atts = {} # attributes for the HTML tag
+ # alignment is handled by CSS rules
+ if 'align' in node:
+ atts['class'] = 'align-%s' % node['align']
+ # set size with "style" attribute (more universal, accepts dimensions)
+ size_declaration = self.image_size(node)
+ if size_declaration:
+ atts['style'] = size_declaration
+
+ # ``:loading:`` option (embed, link, lazy), default from setting,
+ # exception: only embed videos if told via directive option
+ loading = 'link' if mimetype in self.videotypes else self.image_loading
+ loading = node.get('loading', loading)
+ if loading == 'lazy':
+ atts['loading'] = 'lazy'
+ elif loading == 'embed':
+ try:
+ imagepath = self.uri2imagepath(uri)
+ with open(imagepath, 'rb') as imagefile:
+ imagedata = imagefile.read()
+ except (ValueError, OSError) as err:
+ self.messages.append(self.document.reporter.error(
+ f'Cannot embed image "{uri}":\n {err}', base_node=node))
+ # TODO: get external files with urllib.request (cf. odtwriter)?
+ else:
+ self.settings.record_dependencies.add(imagepath)
+ if mimetype == 'image/svg+xml':
+ element = self.prepare_svg(node, imagedata,
+ size_declaration)
+ else:
+ data64 = base64.b64encode(imagedata).decode()
+ uri = f'data:{mimetype};base64,{data64}'
+
+ # No newlines around inline images (but all images may be nested
+ # in a `reference` node which is a `TextElement` instance):
+ if (not isinstance(node.parent, nodes.TextElement)
+ or isinstance(node.parent, nodes.reference)
+ and not isinstance(node.parent.parent, nodes.TextElement)):
+ suffix = '\n'
+ else:
+ suffix = ''
+
+ if mimetype in self.videotypes:
+ atts['title'] = alt
+ if 'controls' in node['classes']:
+ node['classes'].remove('controls')
+ atts['controls'] = 'controls'
+ element = (self.starttag(node, "video", suffix, src=uri, **atts)
+ + f'{alt}{suffix}'
+ + f'{suffix}')
+ elif mimetype == 'application/x-shockwave-flash':
+ atts['type'] = mimetype
+ element = (self.starttag(node, 'object', '', data=uri, **atts)
+ + f'{alt}{suffix}')
+ elif element: # embedded SVG, see above
+ element += suffix
+ else:
+ atts['alt'] = alt
+ element = self.emptytag(node, 'img', suffix, src=uri, **atts)
+ self.body.append(element)
+ if suffix: # block-element
+ self.report_messages(node)
+
+ def depart_image(self, node):
+ pass
+
+ def visit_inline(self, node):
+ self.body.append(self.starttag(node, 'span', ''))
+
+ def depart_inline(self, node):
+ self.body.append('')
+
+ # footnote and citation labels:
+ def visit_label(self, node):
+ self.body.append('')
+ self.body.append('[')
+ # footnote/citation backrefs:
+ if self.settings.footnote_backlinks:
+ backrefs = node.parent.get('backrefs', [])
+ if len(backrefs) == 1:
+ self.body.append('' % backrefs[0])
+
+ def depart_label(self, node):
+ backrefs = []
+ if self.settings.footnote_backlinks:
+ backrefs = node.parent.get('backrefs', backrefs)
+ if len(backrefs) == 1:
+ self.body.append('')
+ self.body.append(']\n')
+ if len(backrefs) > 1:
+ backlinks = ['%s' % (ref, i)
+ for (i, ref) in enumerate(backrefs, 1)]
+ self.body.append('(%s)\n'
+ % ','.join(backlinks))
+
+ def visit_legend(self, node):
+ self.body.append(self.starttag(node, 'div', CLASS='legend'))
+
+ def depart_legend(self, node):
+ self.body.append('\n')
+
+ def visit_line(self, node):
+ self.body.append(self.starttag(node, 'div', suffix='', CLASS='line'))
+ if not len(node):
+ self.body.append(' ')
+
+ def depart_line(self, node):
+ self.body.append('\n')
+
+ def visit_line_block(self, node):
+ self.body.append(self.starttag(node, 'div', CLASS='line-block'))
+
+ def depart_line_block(self, node):
+ self.body.append('\n')
+
+ def visit_list_item(self, node):
+ self.body.append(self.starttag(node, 'li', ''))
+
+ def depart_list_item(self, node):
+ self.body.append('\n')
+
+ # inline literal
+ def visit_literal(self, node):
+ # special case: "code" role
+ classes = node['classes']
+ if 'code' in classes:
+ # filter 'code' from class arguments
+ classes.pop(classes.index('code'))
+ self.body.append(self.starttag(node, 'code', ''))
+ return
+ self.body.append(
+ self.starttag(node, 'span', '', CLASS='docutils literal'))
+ text = node.astext()
+ if not isinstance(node.parent, nodes.literal_block):
+ text = text.replace('\n', ' ')
+ # Protect text like ``--an-option`` and the regular expression
+ # ``[+]?(\d+(\.\d*)?|\.\d+)`` from bad line wrapping
+ for token in self.words_and_spaces.findall(text):
+ if token.strip() and self.in_word_wrap_point.search(token):
+ self.body.append('%s'
+ % self.encode(token))
+ else:
+ self.body.append(self.encode(token))
+ self.body.append('')
+ raise nodes.SkipNode # content already processed
+
+ def depart_literal(self, node):
+ # skipped unless literal element is from "code" role:
+ self.body.append('')
+
+ def visit_literal_block(self, node):
+ self.body.append(self.starttag(node, 'pre', '', CLASS='literal-block'))
+ if 'code' in node['classes']:
+ self.body.append('')
+
+ def depart_literal_block(self, node):
+ if 'code' in node['classes']:
+ self.body.append('')
+ self.body.append('\n')
+
+ # Mathematics:
+ # As there is no native HTML math support, we provide alternatives
+ # for the math-output: LaTeX and MathJax simply wrap the content,
+ # HTML and MathML also convert the math_code.
+ # HTML element:
+ math_tags = { # format: (inline, block, [class arguments])
+ 'html': ('span', 'div', ['formula']),
+ 'latex': ('tt', 'pre', ['math']),
+ 'mathjax': ('span', 'div', ['math']),
+ 'mathml': ('', 'div', []),
+ 'problematic': ('span', 'pre', ['math', 'problematic']),
+ }
+
+ def visit_math(self, node):
+ # Also called from `visit_math_block()`:
+ is_block = isinstance(node, nodes.math_block)
+ format = self.math_output
+ math_code = node.astext().translate(unichar2tex.uni2tex_table)
+
+ # preamble code and conversion
+ if format == 'html':
+ if self.math_options and not self.math_header:
+ self.math_header = [
+ self.stylesheet_call(utils.find_file_in_dirs(
+ s, self.settings.stylesheet_dirs), adjust_path=True)
+ for s in self.math_options.split(',')]
+ math2html.DocumentParameters.displaymode = is_block
+ # TODO: fix display mode in matrices and fractions
+ math_code = wrap_math_code(math_code, is_block)
+ math_code = math2html.math2html(math_code)
+ elif format == 'latex':
+ math_code = self.encode(math_code)
+ elif format == 'mathjax':
+ if not self.math_header:
+ if self.math_options:
+ self.mathjax_url = self.math_options
+ else:
+ self.document.reporter.warning(
+ 'No MathJax URL specified, using local fallback '
+ '(see config.html).', base_node=node)
+ # append MathJax configuration
+ # (input LaTeX with AMS, output common HTML):
+ if '?' not in self.mathjax_url:
+ self.mathjax_url += '?config=TeX-AMS_CHTML'
+ self.math_header = [self.mathjax_script % self.mathjax_url]
+ if is_block:
+ math_code = wrap_math_code(math_code, is_block)
+ else:
+ math_code = rf'\({math_code}\)'
+ math_code = self.encode(math_code)
+ elif format == 'mathml':
+ if 'XHTML 1' in self.doctype:
+ self.doctype = self.doctype_mathml
+ self.content_type = self.content_type_mathml
+ if self.math_options:
+ converter = getattr(tex2mathml_extern, self.math_options)
+ else:
+ converter = latex2mathml.tex2mathml
+ try:
+ math_code = converter(math_code, as_block=is_block)
+ except (MathError, OSError) as err:
+ details = getattr(err, 'details', [])
+ self.messages.append(self.document.reporter.warning(
+ err, *details, base_node=node))
+ math_code = self.encode(node.astext())
+ if self.settings.report_level <= 2:
+ format = 'problematic'
+ else:
+ format = 'latex'
+ if isinstance(err, OSError):
+ # report missing converter only once
+ self.math_output = format
+
+ # append to document body
+ tag = self.math_tags[format][is_block]
+ suffix = '\n' if is_block else ''
+ if tag:
+ self.body.append(self.starttag(node, tag, suffix=suffix,
+ classes=self.math_tags[format][2]))
+ self.body.extend([math_code, suffix])
+ if tag:
+ self.body.append(f'{tag}>{suffix}')
+ # Content already processed:
+ raise nodes.SkipChildren
+
+ def depart_math(self, node):
+ pass
+
+ def visit_math_block(self, node):
+ self.visit_math(node)
+
+ def depart_math_block(self, node):
+ self.report_messages(node)
+
+ # Meta tags: 'lang' attribute replaced by 'xml:lang' in XHTML 1.1
+ # HTML5/polyglot recommends using both
+ def visit_meta(self, node):
+ self.meta.append(self.emptytag(node, 'meta',
+ **node.non_default_attributes()))
+
+ def depart_meta(self, node):
+ pass
+
+ def visit_option(self, node):
+ self.body.append(self.starttag(node, 'span', '', CLASS='option'))
+
+ def depart_option(self, node):
+ self.body.append('')
+ if isinstance(node.next_node(descend=False, siblings=True),
+ nodes.option):
+ self.body.append(', ')
+
+ def visit_option_argument(self, node):
+ self.body.append(node.get('delimiter', ' '))
+ self.body.append(self.starttag(node, 'var', ''))
+
+ def depart_option_argument(self, node):
+ self.body.append('')
+
+ def visit_option_group(self, node):
+ self.body.append(self.starttag(node, 'dt', ''))
+ self.body.append('')
+
+ def depart_option_group(self, node):
+ self.body.append('\n')
+
+ def visit_option_list(self, node):
+ self.body.append(
+ self.starttag(node, 'dl', CLASS='option-list'))
+
+ def depart_option_list(self, node):
+ self.body.append('\n')
+
+ def visit_option_list_item(self, node):
+ pass
+
+ def depart_option_list_item(self, node):
+ pass
+
+ def visit_option_string(self, node):
+ pass
+
+ def depart_option_string(self, node):
+ pass
+
+ def visit_organization(self, node):
+ self.visit_docinfo_item(node, 'organization')
+
+ def depart_organization(self, node):
+ self.depart_docinfo_item()
+
+ # Do not omit
tags
+ # --------------------
+ #
+ # The HTML4CSS1 writer does this to "produce
+ # visually compact lists (less vertical whitespace)". This writer
+ # relies on CSS rules for visual compactness.
+ #
+ # * In XHTML 1.1, e.g., a
element may not contain
+ # character data, so you cannot drop the
tags.
+ # * Keeping simple paragraphs in the field_body enables a CSS
+ # rule to start the field-body on a new line if the label is too long
+ # * it makes the code simpler.
+ #
+ # TODO: omit paragraph tags in simple table cells?
+
+ def visit_paragraph(self, node):
+ self.body.append(self.starttag(node, 'p', ''))
+
+ def depart_paragraph(self, node):
+ self.body.append('
')
+ if not (isinstance(node.parent, (nodes.list_item, nodes.entry))
+ and (len(node.parent) == 1)):
+ self.body.append('\n')
+ self.report_messages(node)
+
+ def visit_problematic(self, node):
+ if node.hasattr('refid'):
+ self.body.append('' % node['refid'])
+ self.context.append('')
+ else:
+ self.context.append('')
+ self.body.append(self.starttag(node, 'span', '', CLASS='problematic'))
+
+ def depart_problematic(self, node):
+ self.body.append('')
+ self.body.append(self.context.pop())
+
+ def visit_raw(self, node):
+ if 'html' in node.get('format', '').split():
+ if isinstance(node.parent, nodes.TextElement):
+ tagname = 'span'
+ else:
+ tagname = 'div'
+ if node['classes']:
+ self.body.append(self.starttag(node, tagname, suffix=''))
+ self.body.append(node.astext())
+ if node['classes']:
+ self.body.append('%s>' % tagname)
+ # Keep non-HTML raw text out of output:
+ raise nodes.SkipNode
+
+ def visit_reference(self, node):
+ atts = {'classes': ['reference']}
+ suffix = ''
+ if 'refuri' in node:
+ atts['href'] = node['refuri']
+ if (self.settings.cloak_email_addresses
+ and atts['href'].startswith('mailto:')):
+ atts['href'] = self.cloak_mailto(atts['href'])
+ self.in_mailto = True
+ atts['classes'].append('external')
+ else:
+ assert 'refid' in node, \
+ 'References must have "refuri" or "refid" attribute.'
+ atts['href'] = '#' + node['refid']
+ atts['classes'].append('internal')
+ if len(node) == 1 and isinstance(node[0], nodes.image):
+ atts['classes'].append('image-reference')
+ if not isinstance(node.parent, nodes.TextElement):
+ suffix = '\n'
+ self.body.append(self.starttag(node, 'a', suffix, **atts))
+
+ def depart_reference(self, node):
+ self.body.append('')
+ if not isinstance(node.parent, nodes.TextElement):
+ self.body.append('\n')
+ self.in_mailto = False
+
+ def visit_revision(self, node):
+ self.visit_docinfo_item(node, 'revision', meta=False)
+
+ def depart_revision(self, node):
+ self.depart_docinfo_item()
+
+ def visit_row(self, node):
+ self.body.append(self.starttag(node, 'tr', ''))
+ node.column = 0
+
+ def depart_row(self, node):
+ self.body.append('\n')
+
+ def visit_rubric(self, node):
+ self.body.append(self.starttag(node, 'p', '', CLASS='rubric'))
+
+ def depart_rubric(self, node):
+ self.body.append('\n')
+
+ def visit_section(self, node):
+ self.section_level += 1
+ self.body.append(
+ self.starttag(node, 'div', CLASS='section'))
+
+ def depart_section(self, node):
+ self.section_level -= 1
+ self.body.append('\n')
+
+ # TODO: use the new HTML5 element \n')
+
+ def visit_table(self, node):
+ atts = {'classes': self.settings.table_style.replace(',', ' ').split()}
+ if 'align' in node:
+ atts['classes'].append('align-%s' % node['align'])
+ if 'width' in node:
+ atts['style'] = 'width: %s;' % node['width']
+ tag = self.starttag(node, 'table', **atts)
+ self.body.append(tag)
+
+ def depart_table(self, node):
+ self.body.append('\n')
+ self.report_messages(node)
+
+ def visit_target(self, node):
+ if ('refuri' not in node
+ and 'refid' not in node
+ and 'refname' not in node):
+ self.body.append(self.starttag(node, 'span', '', CLASS='target'))
+ self.context.append('')
+ else:
+ self.context.append('')
+
+ def depart_target(self, node):
+ self.body.append(self.context.pop())
+
+ # no hard-coded vertical alignment in table body
+ def visit_tbody(self, node):
+ self.body.append(self.starttag(node, 'tbody'))
+
+ def depart_tbody(self, node):
+ self.body.append('\n')
+
+ def visit_term(self, node):
+ if 'details' in node.parent.parent['classes']:
+ self.body.append(self.starttag(node, 'summary', suffix=''))
+ else:
+ # The parent node (definition_list_item) is omitted in HTML.
+ self.body.append(self.starttag(node, 'dt', suffix='',
+ classes=node.parent['classes'],
+ ids=node.parent['ids']))
+
+ def depart_term(self, node):
+ # Nest (optional) classifier(s) in the
element
+ if node.next_node(nodes.classifier, descend=False, siblings=True):
+ return # skip (depart_classifier() calls this function again)
+ if 'details' in node.parent.parent['classes']:
+ self.body.append('\n')
+ else:
+ self.body.append('