""" sphinx.writers.html ~~~~~~~~~~~~~~~~~~~ docutils writers handling Sphinx' custom nodes. :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import copy import os import posixpath import re import warnings from typing import Any, Iterable, Tuple, cast from docutils import nodes from docutils.nodes import Element, Node, Text from docutils.writers.html4css1 import HTMLTranslator as BaseTranslator from docutils.writers.html4css1 import Writer from sphinx import addnodes from sphinx.builders import Builder from sphinx.deprecation import RemovedInSphinx40Warning from sphinx.locale import _, __, admonitionlabels from sphinx.util import logging from sphinx.util.docutils import SphinxTranslator from sphinx.util.images import get_image_size if False: # For type annotation from sphinx.builders.html import StandaloneHTMLBuilder 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 def multiply_length(length: str, scale: int) -> str: """Multiply *length* (width or height) by *scale*.""" matched = re.match(r'^(\d*\.?\d*)\s*(\S*)$', length) if not matched: return length elif scale == 100: return length else: amount, unit = matched.groups() result = float(amount) * scale / 100 return "%s%s" % (int(result), unit) class HTMLWriter(Writer): # override embed-stylesheet default value to 0. settings_spec = copy.deepcopy(Writer.settings_spec) for _setting in settings_spec[2]: if '--embed-stylesheet' in _setting[1]: _setting[2]['default'] = 0 def __init__(self, builder: "StandaloneHTMLBuilder") -> None: super().__init__() self.builder = builder def translate(self) -> None: # sadly, this is mostly copied from parent class visitor = self.builder.create_translator(self.document, self.builder) self.visitor = cast(HTMLTranslator, visitor) self.document.walkabout(visitor) self.output = self.visitor.astext() for attr in ('head_prefix', 'stylesheet', 'head', 'body_prefix', 'body_pre_docinfo', 'docinfo', 'body', 'fragment', 'body_suffix', 'meta', 'title', 'subtitle', 'header', 'footer', 'html_prolog', 'html_head', 'html_title', 'html_subtitle', 'html_body', ): setattr(self, attr, getattr(visitor, attr, None)) self.clean_meta = ''.join(self.visitor.meta[2:]) class HTMLTranslator(SphinxTranslator, BaseTranslator): """ Our custom HTML translator. """ builder = None # type: StandaloneHTMLBuilder def __init__(self, *args: Any) -> None: if isinstance(args[0], nodes.document) and isinstance(args[1], Builder): document, builder = args else: warnings.warn('The order of arguments for HTMLTranslator has been changed. ' 'Please give "document" as 1st and "builder" as 2nd.', RemovedInSphinx40Warning, stacklevel=2) builder, document = args super().__init__(document, builder) self.highlighter = self.builder.highlighter self.docnames = [self.builder.current_docname] # for singlehtml builder self.manpages_url = self.config.manpages_url self.protect_literal_text = 0 self.permalink_text = self.config.html_add_permalinks # support backwards-compatible setting to a bool if not isinstance(self.permalink_text, str): self.permalink_text = 'ΒΆ' if self.permalink_text else '' self.permalink_text = self.encode(self.permalink_text) self.secnumber_suffix = self.config.html_secnumber_suffix self.param_separator = '' self.optional_param_level = 0 self._table_row_index = 0 self._fieldlist_row_index = 0 self.required_params_left = 0 def visit_start_of_file(self, node: Element) -> 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: Element) -> None: self.docnames.pop() def visit_desc(self, node: Element) -> None: self.body.append(self.starttag(node, 'dl', CLASS=node['objtype'])) def depart_desc(self, node: Element) -> None: self.body.append('\n\n') def visit_desc_signature(self, node: Element) -> None: # the id is set automatically self.body.append(self.starttag(node, 'dt')) def depart_desc_signature(self, node: Element) -> 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: Element) -> None: pass def depart_desc_signature_line(self, node: Element) -> 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: Element) -> None: self.body.append(self.starttag(node, 'code', '', CLASS='descclassname')) def depart_desc_addname(self, node: Element) -> None: self.body.append('') def visit_desc_type(self, node: Element) -> None: pass def depart_desc_type(self, node: Element) -> None: pass def visit_desc_returns(self, node: Element) -> None: self.body.append(' → ') def depart_desc_returns(self, node: Element) -> None: pass def visit_desc_name(self, node: Element) -> None: self.body.append(self.starttag(node, 'code', '', CLASS='descname')) def depart_desc_name(self, node: Element) -> None: self.body.append('') def visit_desc_parameterlist(self, node: Element) -> 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: Element) -> 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: Element) -> 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: Element) -> 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: Element) -> None: self.optional_param_level += 1 self.body.append('[') def depart_desc_optional(self, node: Element) -> None: self.optional_param_level -= 1 self.body.append(']') def visit_desc_annotation(self, node: Element) -> None: self.body.append(self.starttag(node, 'em', '', CLASS='property')) def depart_desc_annotation(self, node: Element) -> None: self.body.append('') def visit_desc_content(self, node: Element) -> None: self.body.append(self.starttag(node, 'dd', '')) def depart_desc_content(self, node: Element) -> None: self.body.append('') def visit_versionmodified(self, node: Element) -> None: self.body.append(self.starttag(node, 'div', CLASS=node['type'])) def depart_versionmodified(self, node: Element) -> None: self.body.append('\n') # overwritten def visit_reference(self, node: Element) -> 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 = True 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: Element) -> None: self.visit_reference(node) def depart_number_reference(self, node: Element) -> None: self.depart_reference(node) # overwritten -- we don't want source comments to show up in the HTML def visit_comment(self, node: Element) -> None: # type: ignore raise nodes.SkipNode # overwritten def visit_admonition(self, node: Element, name: str = '') -> None: self.body.append(self.starttag( node, 'div', CLASS=('admonition ' + name))) if name: node.insert(0, nodes.title(name, admonitionlabels[name])) self.set_first_last(node) def visit_seealso(self, node: Element) -> None: self.visit_admonition(node, 'seealso') def depart_seealso(self, node: Element) -> None: self.depart_admonition(node) def get_secnumber(self, node: Element) -> Tuple[int, ...]: if node.get('secnumber'): return node['secnumber'] 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): return self.builder.secnumbers[anchorname] return None def add_secnumber(self, node: Element) -> None: secnumber = self.get_secnumber(node) if secnumber: self.body.append('%s' % ('.'.join(map(str, secnumber)) + self.secnumber_suffix)) def add_fignumber(self, node: Element) -> None: def append_fignumber(figtype: str, figure_id: str) -> None: if self.builder.name == 'singlehtml': key = "%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_enumerable_node_type(node) 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: Element, title: str) -> None: if node['ids'] and self.permalink_text and self.builder.add_permalinks: format = '%s' self.body.append(format % (node['ids'][0], title, self.permalink_text)) def generate_targets_for_listing(self, node: Element) -> None: """Generate hyperlink targets for listings. Original visit_bullet_list(), visit_definition_list() and visit_enumerated_list() generates hyperlink targets inside listing tags (