# -*- coding: utf-8 -*-
"""
sphinx.writers.html5
~~~~~~~~~~~~~~~~~~~~
Experimental docutils writers for HTML5 handling Sphinx' custom nodes.
:copyright: Copyright 2007-2018 by the Sphinx team, see AUTHORS.
:license: BSD, see LICENSE for details.
"""
import sys
import posixpath
import os
from six import string_types
from docutils import nodes
from docutils.writers.html5_polyglot import HTMLTranslator as BaseTranslator
from sphinx import addnodes
from sphinx.locale import admonitionlabels, _
from sphinx.util import logging
from sphinx.util.images import get_image_size
if False:
# For type annotation
from typing import Any # NOQA
from sphinx.builders.html import StandaloneHTMLBuilder # NOQA
logger = logging.getLogger(__name__)
# A good overview of the purpose behind these classes can be found here:
# http://www.arnebrodowski.de/blog/write-your-own-restructuredtext-writer.html
class HTML5Translator(BaseTranslator):
"""
Our custom HTML translator.
"""
def __init__(self, builder, *args, **kwds):
# type: (StandaloneHTMLBuilder, Any, Any) -> None
BaseTranslator.__init__(self, *args, **kwds)
self.highlighter = builder.highlighter
self.builder = builder
self.highlightlang = self.highlightlang_base = \
builder.config.highlight_language
self.highlightopts = builder.config.highlight_options
self.highlightlinenothreshold = sys.maxsize
self.docnames = [builder.current_docname] # for singlehtml builder
self.protect_literal_text = 0
self.permalink_text = builder.config.html_add_permalinks
# support backwards-compatible setting to a bool
if not isinstance(self.permalink_text, string_types):
self.permalink_text = self.permalink_text and u'\u00B6' or ''
self.permalink_text = self.encode(self.permalink_text)
self.secnumber_suffix = builder.config.html_secnumber_suffix
self.param_separator = ''
self.optional_param_level = 0
self._table_row_index = 0
self.required_params_left = 0
def visit_start_of_file(self, node):
# type: (nodes.Node) -> None
# only occurs in the single-file builder
self.docnames.append(node['docname'])
self.body.append('' % node['docname'])
def depart_start_of_file(self, node):
# type: (nodes.Node) -> None
self.docnames.pop()
def visit_desc(self, node):
# type: (nodes.Node) -> None
self.body.append(self.starttag(node, 'dl', CLASS=node['objtype']))
def depart_desc(self, node):
# type: (nodes.Node) -> None
self.body.append('\n\n')
def visit_desc_signature(self, node):
# type: (nodes.Node) -> None
# the id is set automatically
self.body.append(self.starttag(node, 'dt'))
# anchor for per-desc interactive data
if node.parent['objtype'] != 'describe' \
and node['ids'] and node['first']:
self.body.append('' % node['ids'][0])
def depart_desc_signature(self, node):
# type: (nodes.Node) -> None
if not node.get('is_multiline'):
self.add_permalink_ref(node, _('Permalink to this definition'))
self.body.append('\n')
def visit_desc_signature_line(self, node):
# type: (nodes.Node) -> None
pass
def depart_desc_signature_line(self, node):
# type: (nodes.Node) -> None
if node.get('add_permalink'):
# the permalink info is on the parent desc_signature node
self.add_permalink_ref(node.parent, _('Permalink to this definition'))
self.body.append('
')
def visit_desc_addname(self, node):
# type: (nodes.Node) -> None
self.body.append(self.starttag(node, 'code', '', CLASS='descclassname'))
def depart_desc_addname(self, node):
# type: (nodes.Node) -> None
self.body.append('')
def visit_desc_type(self, node):
# type: (nodes.Node) -> None
pass
def depart_desc_type(self, node):
# type: (nodes.Node) -> None
pass
def visit_desc_returns(self, node):
# type: (nodes.Node) -> None
self.body.append(' → ')
def depart_desc_returns(self, node):
# type: (nodes.Node) -> None
pass
def visit_desc_name(self, node):
# type: (nodes.Node) -> None
self.body.append(self.starttag(node, 'code', '', CLASS='descname'))
def depart_desc_name(self, node):
# type: (nodes.Node) -> None
self.body.append('')
def visit_desc_parameterlist(self, node):
# type: (nodes.Node) -> None
self.body.append('(')
self.first_param = 1
self.optional_param_level = 0
# How many required parameters are left.
self.required_params_left = sum([isinstance(c, addnodes.desc_parameter)
for c in node.children])
self.param_separator = node.child_text_separator
def depart_desc_parameterlist(self, node):
# type: (nodes.Node) -> None
self.body.append(')')
# If required parameters are still to come, then put the comma after
# the parameter. Otherwise, put the comma before. This ensures that
# signatures like the following render correctly (see issue #1001):
#
# foo([a, ]b, c[, d])
#
def visit_desc_parameter(self, node):
# type: (nodes.Node) -> None
if self.first_param:
self.first_param = 0
elif not self.required_params_left:
self.body.append(self.param_separator)
if self.optional_param_level == 0:
self.required_params_left -= 1
if not node.hasattr('noemph'):
self.body.append('')
def depart_desc_parameter(self, node):
# type: (nodes.Node) -> None
if not node.hasattr('noemph'):
self.body.append('')
if self.required_params_left:
self.body.append(self.param_separator)
def visit_desc_optional(self, node):
# type: (nodes.Node) -> None
self.optional_param_level += 1
self.body.append('[')
def depart_desc_optional(self, node):
# type: (nodes.Node) -> None
self.optional_param_level -= 1
self.body.append(']')
def visit_desc_annotation(self, node):
# type: (nodes.Node) -> None
self.body.append(self.starttag(node, 'em', '', CLASS='property'))
def depart_desc_annotation(self, node):
# type: (nodes.Node) -> None
self.body.append('')
def visit_desc_content(self, node):
# type: (nodes.Node) -> None
self.body.append(self.starttag(node, 'dd', ''))
def depart_desc_content(self, node):
# type: (nodes.Node) -> None
self.body.append('')
def visit_versionmodified(self, node):
# type: (nodes.Node) -> None
self.body.append(self.starttag(node, 'div', CLASS=node['type']))
def depart_versionmodified(self, node):
# type: (nodes.Node) -> None
self.body.append('\n')
# overwritten
def visit_reference(self, node):
# type: (nodes.Node) -> None
atts = {'class': 'reference'}
if node.get('internal') or 'refuri' not in node:
atts['class'] += ' internal'
else:
atts['class'] += ' external'
if 'refuri' in node:
atts['href'] = node['refuri'] or '#'
if self.settings.cloak_email_addresses and \
atts['href'].startswith('mailto:'):
atts['href'] = self.cloak_mailto(atts['href'])
self.in_mailto = 1
else:
assert 'refid' in node, \
'References must have "refuri" or "refid" attribute.'
atts['href'] = '#' + node['refid']
if not isinstance(node.parent, nodes.TextElement):
assert len(node) == 1 and isinstance(node[0], nodes.image)
atts['class'] += ' image-reference'
if 'reftitle' in node:
atts['title'] = node['reftitle']
if 'target' in node:
atts['target'] = node['target']
self.body.append(self.starttag(node, 'a', '', **atts))
if node.get('secnumber'):
self.body.append(('%s' + self.secnumber_suffix) %
'.'.join(map(str, node['secnumber'])))
def visit_number_reference(self, node):
# type: (nodes.Node) -> None
self.visit_reference(node)
def depart_number_reference(self, node):
# type: (nodes.Node) -> None
self.depart_reference(node)
# overwritten -- we don't want source comments to show up in the HTML
def visit_comment(self, node):
# type: (nodes.Node) -> None
raise nodes.SkipNode
# overwritten
def visit_admonition(self, node, name=''):
# type: (nodes.Node, unicode) -> None
self.body.append(self.starttag(
node, 'div', CLASS=('admonition ' + name)))
if name:
node.insert(0, nodes.title(name, admonitionlabels[name]))
def visit_seealso(self, node):
# type: (nodes.Node) -> None
self.visit_admonition(node, 'seealso')
def depart_seealso(self, node):
# type: (nodes.Node) -> None
self.depart_admonition(node)
def add_secnumber(self, node):
# type: (nodes.Node) -> None
if node.get('secnumber'):
self.body.append('.'.join(map(str, node['secnumber'])) +
self.secnumber_suffix)
elif isinstance(node.parent, nodes.section):
if self.builder.name == 'singlehtml':
docname = self.docnames[-1]
anchorname = "%s/#%s" % (docname, node.parent['ids'][0])
if anchorname not in self.builder.secnumbers:
anchorname = "%s/" % docname # try first heading which has no anchor
else:
anchorname = '#' + node.parent['ids'][0]
if anchorname not in self.builder.secnumbers:
anchorname = '' # try first heading which has no anchor
if self.builder.secnumbers.get(anchorname):
numbers = self.builder.secnumbers[anchorname]
self.body.append('.'.join(map(str, numbers)) +
self.secnumber_suffix)
def add_fignumber(self, node):
# type: (nodes.Node) -> None
def append_fignumber(figtype, figure_id):
# type: (unicode, unicode) -> None
if self.builder.name == 'singlehtml':
key = u"%s/%s" % (self.docnames[-1], figtype)
else:
key = figtype
if figure_id in self.builder.fignumbers.get(key, {}):
self.body.append('')
prefix = self.builder.config.numfig_format.get(figtype)
if prefix is None:
msg = 'numfig_format is not defined for %s' % figtype
logger.warning(msg)
else:
numbers = self.builder.fignumbers[key][figure_id]
self.body.append(prefix % '.'.join(map(str, numbers)) + ' ')
self.body.append('')
figtype = self.builder.env.domains['std'].get_figtype(node) # type: ignore
if figtype:
if len(node['ids']) == 0:
msg = 'Any IDs not assigned for %s node' % node.tagname
logger.warning(msg, location=node)
else:
append_fignumber(figtype, node['ids'][0])
def add_permalink_ref(self, node, title):
# type: (nodes.Node, unicode) -> None
if node['ids'] and self.permalink_text and self.builder.add_permalinks:
format = u'%s'
self.body.append(format % (node['ids'][0], title, self.permalink_text))
# overwritten
def visit_bullet_list(self, node):
# type: (nodes.Node) -> None
if len(node) == 1 and node[0].tagname == 'toctree':
# avoid emitting empty
tags around paragraph can be omitted.""" if isinstance(node.parent, addnodes.desc_content): # Never compact desc_content items. return False if isinstance(node.parent, addnodes.versionmodified): # Never compact versionmodified nodes. return False return BaseTranslator.should_be_compact_paragraph(self, node) def visit_compact_paragraph(self, node): # type: (nodes.Node) -> None pass def depart_compact_paragraph(self, node): # type: (nodes.Node) -> None pass def visit_highlightlang(self, node): # type: (nodes.Node) -> None self.highlightlang = node['lang'] self.highlightlinenothreshold = node['linenothreshold'] def depart_highlightlang(self, node): # type: (nodes.Node) -> None pass def visit_download_reference(self, node): # type: (nodes.Node) -> None if self.builder.download_support and node.hasattr('filename'): self.body.append( '' % posixpath.join(self.builder.dlpath, node['filename'])) self.context.append('') else: self.context.append('') def depart_download_reference(self, node): # type: (nodes.Node) -> None self.body.append(self.context.pop()) # overwritten def visit_image(self, node): # type: (nodes.Node) -> None olduri = node['uri'] # rewrite the URI if the environment knows about it if olduri in self.builder.images: node['uri'] = posixpath.join(self.builder.imgpath, self.builder.images[olduri]) uri = node['uri'] if uri.lower().endswith(('svg', 'svgz')): atts = {'src': uri} if 'width' in node: atts['width'] = node['width'] if 'height' in node: atts['height'] = node['height'] atts['alt'] = node.get('alt', uri) if 'align' in node: self.body.append('