Add SphinxTranslator as an abstract class

This commit is contained in:
Takeshi KOMIYA 2018-11-28 01:53:00 +09:00
parent e888e92ac4
commit 3863256cb6
9 changed files with 134 additions and 99 deletions

View File

@ -33,8 +33,9 @@ from sphinx.util.osutil import ensuredir, ENOENT, EPIPE, EINVAL
if False:
# For type annotation
from docutils.parsers.rst import Directive # NOQA
from typing import Any, Dict, List, Tuple, Union # NOQA
from typing import Any, Dict, List, Tuple # NOQA
from sphinx.application import Sphinx # NOQA
from sphinx.util.docutils import SphinxTranslator # NOQA
from sphinx.util.typing import unicode # NOQA
from sphinx.writers.html import HTMLTranslator # NOQA
from sphinx.writers.latex import LaTeXTranslator # NOQA
@ -219,7 +220,7 @@ class GraphvizSimple(SphinxDirective):
def render_dot(self, code, options, format, prefix='graphviz'):
# type: (Union[HTMLTranslator, LaTeXTranslator, TexinfoTranslator], unicode, Dict, unicode, unicode) -> Tuple[unicode, unicode] # NOQA
# type: (SphinxTranslator, unicode, Dict, unicode, unicode) -> Tuple[unicode, unicode] # NOQA
"""Render graphviz code into a PNG or PDF output file."""
graphviz_dot = options.get('graphviz_dot', self.builder.config.graphviz_dot)
hashkey = (code + str(options) + str(graphviz_dot) +

View File

@ -39,6 +39,7 @@ if False:
from types import ModuleType # NOQA
from typing import Any, Callable, Generator, List, Set, Tuple, Type # NOQA
from docutils.statemachine import State, ViewList # NOQA
from sphinx.builders import Builder # NOQA
from sphinx.config import Config # NOQA
from sphinx.environment import BuildEnvironment # NOQA
from sphinx.io import SphinxFileInput # NOQA
@ -383,6 +384,32 @@ class SphinxDirective(Directive):
return self.env.config
class SphinxTranslator(nodes.NodeVisitor):
"""A base class for Sphinx translators.
This class provides helper methods for Sphinx translators.
.. note:: The subclasses of this class might not work with docutils.
This class is strongly coupled with Sphinx.
"""
def __init__(self, builder, document):
# type: (Builder, nodes.document) -> None
super(SphinxTranslator, self).__init__(document)
self.builder = builder
self.config = builder.config
def get_settings(self):
# type: () -> Any
"""Get settings object with type safe.
.. note:: It is hard to check types for settings object because it's attributes
are added dynamically. This method avoids the type errors through
imitating it's type as Any.
"""
return self.document.settings
# cache a vanilla instance of nodes.document
# Used in new_document() function
__document_cache__ = None # type: nodes.document

View File

@ -23,6 +23,7 @@ from sphinx import addnodes
from sphinx.deprecation import RemovedInSphinx30Warning
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:
@ -67,25 +68,26 @@ class HTMLWriter(Writer):
self.clean_meta = ''.join(self.visitor.meta[2:])
class HTMLTranslator(BaseTranslator):
class HTMLTranslator(SphinxTranslator, BaseTranslator):
"""
Our custom HTML translator.
"""
def __init__(self, builder, *args, **kwds):
# type: (StandaloneHTMLBuilder, Any, Any) -> None
super(HTMLTranslator, self).__init__(*args, **kwds)
self.highlighter = builder.highlighter
self.builder = builder
self.docnames = [builder.current_docname] # for singlehtml builder
self.manpages_url = builder.config.manpages_url
builder = None # type: StandaloneHTMLBuilder
def __init__(self, builder, document):
# type: (StandaloneHTMLBuilder, nodes.document) -> None
super(HTMLTranslator, self).__init__(builder, document)
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 = builder.config.html_add_permalinks
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 = self.permalink_text and u'\u00B6' or ''
self.permalink_text = self.encode(self.permalink_text)
self.secnumber_suffix = builder.config.html_secnumber_suffix
self.secnumber_suffix = self.config.html_secnumber_suffix
self.param_separator = ''
self.optional_param_level = 0
self._table_row_index = 0
@ -250,8 +252,8 @@ class HTMLTranslator(BaseTranslator):
atts['class'] += ' external'
if 'refuri' in node:
atts['href'] = node['refuri'] or '#'
if self.settings.cloak_email_addresses and \
atts['href'].startswith('mailto:'):
if (self.get_settings().cloak_email_addresses and
atts['href'].startswith('mailto:')):
atts['href'] = self.cloak_mailto(atts['href'])
self.in_mailto = True
else:
@ -708,7 +710,7 @@ class HTMLTranslator(BaseTranslator):
# protect runs of multiple spaces; the last one can wrap
self.body.append(' ' * (len(token) - 1) + ' ')
else:
if self.in_mailto and self.settings.cloak_email_addresses:
if self.in_mailto and self.get_settings().cloak_email_addresses:
encoded = self.cloak_email(encoded)
self.body.append(encoded)

View File

@ -22,6 +22,7 @@ from sphinx import addnodes
from sphinx.deprecation import RemovedInSphinx30Warning
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:
@ -37,25 +38,26 @@ logger = logging.getLogger(__name__)
# http://www.arnebrodowski.de/blog/write-your-own-restructuredtext-writer.html
class HTML5Translator(BaseTranslator):
class HTML5Translator(SphinxTranslator, BaseTranslator):
"""
Our custom HTML translator.
"""
def __init__(self, builder, *args, **kwds):
# type: (StandaloneHTMLBuilder, Any, Any) -> None
super(HTML5Translator, self).__init__(*args, **kwds)
self.highlighter = builder.highlighter
self.builder = builder
self.docnames = [builder.current_docname] # for singlehtml builder
self.manpages_url = builder.config.manpages_url
builder = None # type: StandaloneHTMLBuilder
def __init__(self, builder, document):
# type: (StandaloneHTMLBuilder, nodes.document) -> None
super(HTML5Translator, self).__init__(builder, document)
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 = builder.config.html_add_permalinks
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 = self.permalink_text and u'\u00B6' or ''
self.permalink_text = self.encode(self.permalink_text)
self.secnumber_suffix = builder.config.html_secnumber_suffix
self.secnumber_suffix = self.config.html_secnumber_suffix
self.param_separator = ''
self.optional_param_level = 0
self._table_row_index = 0
@ -219,8 +221,8 @@ class HTML5Translator(BaseTranslator):
atts['class'] += ' external'
if 'refuri' in node:
atts['href'] = node['refuri'] or '#'
if self.settings.cloak_email_addresses and \
atts['href'].startswith('mailto:'):
if (self.get_settings().cloak_email_addresses and
atts['href'].startswith('mailto:')):
atts['href'] = self.cloak_mailto(atts['href'])
self.in_mailto = True
else:
@ -649,7 +651,7 @@ class HTML5Translator(BaseTranslator):
# protect runs of multiple spaces; the last one can wrap
self.body.append(' ' * (len(token) - 1) + ' ')
else:
if self.in_mailto and self.settings.cloak_email_addresses:
if self.in_mailto and self.get_settings().cloak_email_addresses:
encoded = self.cloak_email(encoded)
self.body.append(encoded)
@ -788,7 +790,7 @@ class HTML5Translator(BaseTranslator):
self._table_row_index = 0
classes = [cls.strip(u' \t\n')
for cls in self.settings.table_style.split(',')]
for cls in self.get_settings().table_style.split(',')]
classes.insert(0, "docutils") # compat
if 'align' in node:
classes.append('align-%s' % node['align'])

View File

@ -29,6 +29,7 @@ from sphinx.deprecation import RemovedInSphinx30Warning, RemovedInSphinx40Warnin
from sphinx.errors import SphinxError
from sphinx.locale import admonitionlabels, _, __
from sphinx.util import split_into, logging
from sphinx.util.docutils import SphinxTranslator
from sphinx.util.i18n import format_date
from sphinx.util.nodes import clean_astext
from sphinx.util.template import LaTeXRenderer
@ -497,7 +498,8 @@ def rstdim_to_latexdim(width_str):
return res
class LaTeXTranslator(nodes.NodeVisitor):
class LaTeXTranslator(SphinxTranslator):
builder = None # type: LaTeXBuilder
secnumdepth = 2 # legacy sphinxhowto.cls uses this, whereas article.cls
# default is originally 3. For book/report, 2 is already LaTeX default.
@ -508,8 +510,7 @@ class LaTeXTranslator(nodes.NodeVisitor):
def __init__(self, document, builder):
# type: (nodes.document, LaTeXBuilder) -> None
super(LaTeXTranslator, self).__init__(document)
self.builder = builder
super(LaTeXTranslator, self).__init__(builder, document)
self.body = [] # type: List[unicode]
# flags
@ -529,42 +530,42 @@ class LaTeXTranslator(nodes.NodeVisitor):
self.first_param = 0
# sort out some elements
self.elements = builder.context.copy()
self.elements = self.builder.context.copy()
# but some have other interface in config file
self.elements['wrapperclass'] = self.format_docclass(document.settings.docclass)
self.elements['wrapperclass'] = self.format_docclass(self.get_settings().docclass)
# we assume LaTeX class provides \chapter command except in case
# of non-Japanese 'howto' case
self.sectionnames = LATEXSECTIONNAMES[:]
if document.settings.docclass == 'howto':
docclass = builder.config.latex_docclass.get('howto', 'article')
if self.get_settings().docclass == 'howto':
docclass = self.config.latex_docclass.get('howto', 'article')
if docclass[0] == 'j': # Japanese class...
pass
else:
self.sectionnames.remove('chapter')
else:
docclass = builder.config.latex_docclass.get('manual', 'report')
docclass = self.config.latex_docclass.get('manual', 'report')
self.elements['docclass'] = docclass
# determine top section level
self.top_sectionlevel = 1
if builder.config.latex_toplevel_sectioning:
if self.config.latex_toplevel_sectioning:
try:
self.top_sectionlevel = \
self.sectionnames.index(builder.config.latex_toplevel_sectioning)
self.sectionnames.index(self.config.latex_toplevel_sectioning)
except ValueError:
logger.warning(__('unknown %r toplevel_sectioning for class %r') %
(builder.config.latex_toplevel_sectioning, docclass))
(self.config.latex_toplevel_sectioning, docclass))
if builder.config.today:
self.elements['date'] = builder.config.today
if self.config.today:
self.elements['date'] = self.config.today
else:
self.elements['date'] = format_date(builder.config.today_fmt or _('%b %d, %Y'),
language=builder.config.language)
self.elements['date'] = format_date(self.config.today_fmt or _('%b %d, %Y'),
language=self.config.language)
if builder.config.numfig:
self.numfig_secnum_depth = builder.config.numfig_secnum_depth
if self.config.numfig:
self.numfig_secnum_depth = self.config.numfig_secnum_depth
if self.numfig_secnum_depth > 0: # default is 1
# numfig_secnum_depth as passed to sphinx.sty indices same names as in
# LATEXSECTIONNAMES but with -1 for part, 0 for chapter, 1 for section...
@ -582,31 +583,31 @@ class LaTeXTranslator(nodes.NodeVisitor):
else:
self.elements['sphinxpkgoptions'] += ',nonumfigreset'
try:
if builder.config.math_numfig:
if self.config.math_numfig:
self.elements['sphinxpkgoptions'] += ',mathnumfig'
except AttributeError:
pass
if builder.config.latex_logo:
if self.config.latex_logo:
# no need for \\noindent here, used in flushright
self.elements['logo'] = '\\sphinxincludegraphics{%s}\\par' % \
path.basename(builder.config.latex_logo)
path.basename(self.config.latex_logo)
if (builder.config.language and builder.config.language != 'ja' and
'fncychap' not in builder.config.latex_elements):
if (self.config.language and self.config.language != 'ja' and
'fncychap' not in self.config.latex_elements):
# use Sonny style if any language specified
self.elements['fncychap'] = ('\\usepackage[Sonny]{fncychap}\n'
'\\ChNameVar{\\Large\\normalfont'
'\\sffamily}\n\\ChTitleVar{\\Large'
'\\normalfont\\sffamily}')
self.babel = ExtBabel(builder.config.language,
self.babel = ExtBabel(self.config.language,
not self.elements['babel'])
if builder.config.language and not self.babel.is_supported_language():
if self.config.language and not self.babel.is_supported_language():
# emit warning if specified language is invalid
# (only emitting, nothing changed to processing)
logger.warning(__('no Babel option known for language %r'),
builder.config.language)
self.config.language)
# set up multilingual module...
if self.elements['latex_engine'] == 'pdflatex':
@ -628,12 +629,11 @@ class LaTeXTranslator(nodes.NodeVisitor):
self.elements['classoptions'] += ',' + self.babel.get_language()
# this branch is not taken for xelatex/lualatex if default settings
self.elements['multilingual'] = self.elements['babel']
if builder.config.language:
if self.config.language:
self.elements['shorthandoff'] = SHORTHANDOFF
# Times fonts don't work with Cyrillic languages
if self.babel.uses_cyrillic() \
and 'fontpkg' not in builder.config.latex_elements:
if self.babel.uses_cyrillic() and 'fontpkg' not in self.config.latex_elements:
self.elements['fontpkg'] = ''
elif self.elements['polyglossia']:
self.elements['classoptions'] += ',' + self.babel.get_language()
@ -647,25 +647,25 @@ class LaTeXTranslator(nodes.NodeVisitor):
self.elements['multilingual'] = '%s\n%s' % (self.elements['polyglossia'],
mainlanguage)
if getattr(builder, 'usepackages', None):
if getattr(self.builder, 'usepackages', None):
def declare_package(packagename, options=None):
# type:(unicode, unicode) -> unicode
if options:
return '\\usepackage[%s]{%s}' % (options, packagename)
else:
return '\\usepackage{%s}' % (packagename,)
usepackages = (declare_package(*p) for p in builder.usepackages)
usepackages = (declare_package(*p) for p in self.builder.usepackages)
self.elements['usepackages'] += "\n".join(usepackages)
minsecnumdepth = self.secnumdepth # 2 from legacy sphinx manual/howto
if document.get('tocdepth'):
if self.document.get('tocdepth'):
# reduce tocdepth if `part` or `chapter` is used for top_sectionlevel
# tocdepth = -1: show only parts
# tocdepth = 0: show parts and chapters
# tocdepth = 1: show parts, chapters and sections
# tocdepth = 2: show parts, chapters, sections and subsections
# ...
tocdepth = document['tocdepth'] + self.top_sectionlevel - 2
tocdepth = self.document['tocdepth'] + self.top_sectionlevel - 2
if len(self.sectionnames) < len(LATEXSECTIONNAMES) and \
self.top_sectionlevel > 0:
tocdepth += 1 # because top_sectionlevel is shifted by -1
@ -676,16 +676,16 @@ class LaTeXTranslator(nodes.NodeVisitor):
self.elements['tocdepth'] = '\\setcounter{tocdepth}{%d}' % tocdepth
minsecnumdepth = max(minsecnumdepth, tocdepth)
if builder.config.numfig and (builder.config.numfig_secnum_depth > 0):
if self.config.numfig and (self.config.numfig_secnum_depth > 0):
minsecnumdepth = max(minsecnumdepth, self.numfig_secnum_depth - 1)
if minsecnumdepth > self.secnumdepth:
self.elements['secnumdepth'] = '\\setcounter{secnumdepth}{%d}' %\
minsecnumdepth
if getattr(document.settings, 'contentsname', None):
self.elements['contentsname'] = \
self.babel_renewcommand('\\contentsname', document.settings.contentsname)
contentsname = self.get_settings().contentsname
self.elements['contentsname'] = self.babel_renewcommand('\\contentsname',
contentsname)
if self.elements['maxlistdepth']:
self.elements['sphinxpkgoptions'] += (',maxlistdepth=%s' %
@ -699,9 +699,9 @@ class LaTeXTranslator(nodes.NodeVisitor):
if self.elements['extraclassoptions']:
self.elements['classoptions'] += ',' + \
self.elements['extraclassoptions']
self.elements['numfig_format'] = self.generate_numfig_format(builder)
self.elements['numfig_format'] = self.generate_numfig_format(self.builder)
self.highlighter = highlighting.PygmentsBridge('latex', builder.config.pygments_style)
self.highlighter = highlighting.PygmentsBridge('latex', self.config.pygments_style)
self.context = [] # type: List[Any]
self.descstack = [] # type: List[unicode]
self.table = None # type: Table

View File

@ -20,6 +20,7 @@ from docutils.writers.manpage import (
from sphinx import addnodes
from sphinx.locale import admonitionlabels, _
from sphinx.util import logging
from sphinx.util.docutils import SphinxTranslator
from sphinx.util.i18n import format_date
from sphinx.util.nodes import NodeMatcher
@ -78,17 +79,16 @@ class NestedInlineTransform:
node.parent.insert(pos + 1, newnode)
class ManualPageTranslator(BaseTranslator):
class ManualPageTranslator(SphinxTranslator, BaseTranslator):
"""
Custom translator.
"""
_docinfo = {} # type: Dict[unicode, Any]
def __init__(self, builder, *args, **kwds):
# type: (Builder, Any, Any) -> None
super(ManualPageTranslator, self).__init__(*args, **kwds)
self.builder = builder
def __init__(self, builder, document):
# type: (Builder, nodes.document) -> None
super(ManualPageTranslator, self).__init__(builder, document)
self.in_productionlist = 0
@ -96,23 +96,24 @@ class ManualPageTranslator(BaseTranslator):
self.section_level = -1
# docinfo set by man_pages config value
self._docinfo['title'] = self.document.settings.title
self._docinfo['subtitle'] = self.document.settings.subtitle
if self.document.settings.authors:
settings = self.get_settings()
self._docinfo['title'] = settings.title
self._docinfo['subtitle'] = settings.subtitle
if settings.authors:
# don't set it if no author given
self._docinfo['author'] = self.document.settings.authors
self._docinfo['manual_section'] = self.document.settings.section
self._docinfo['author'] = settings.authors
self._docinfo['manual_section'] = settings.section
# docinfo set by other config values
self._docinfo['title_upper'] = self._docinfo['title'].upper()
if builder.config.today:
self._docinfo['date'] = builder.config.today
if self.config.today:
self._docinfo['date'] = self.config.today
else:
self._docinfo['date'] = format_date(builder.config.today_fmt or _('%b %d, %Y'),
language=builder.config.language)
self._docinfo['copyright'] = builder.config.copyright
self._docinfo['version'] = builder.config.version
self._docinfo['manual_group'] = builder.config.project
self._docinfo['date'] = format_date(self.config.today_fmt or _('%b %d, %Y'),
language=self.config.language)
self._docinfo['copyright'] = self.config.copyright
self._docinfo['version'] = self.config.version
self._docinfo['manual_group'] = self.config.project
# Overwrite admonition label translations with our own
for label, translation in admonitionlabels.items():

View File

@ -20,6 +20,7 @@ from sphinx import addnodes, __display_version__
from sphinx.errors import ExtensionError
from sphinx.locale import admonitionlabels, _, __
from sphinx.util import logging
from sphinx.util.docutils import SphinxTranslator
from sphinx.util.i18n import format_date
from sphinx.writers.latex import collected_footnote
@ -144,8 +145,9 @@ class TexinfoWriter(writers.Writer):
setattr(self, attr, getattr(self.visitor, attr))
class TexinfoTranslator(nodes.NodeVisitor):
class TexinfoTranslator(SphinxTranslator):
builder = None # type: TexinfoBuilder
ignore_missing_images = False
default_elements = {
@ -165,8 +167,7 @@ class TexinfoTranslator(nodes.NodeVisitor):
def __init__(self, document, builder):
# type: (nodes.document, TexinfoBuilder) -> None
super(TexinfoTranslator, self).__init__(document)
self.builder = builder
super(TexinfoTranslator, self).__init__(builder, document)
self.init_settings()
self.written_ids = set() # type: Set[unicode]
@ -227,7 +228,7 @@ class TexinfoTranslator(nodes.NodeVisitor):
def init_settings(self):
# type: () -> None
settings = self.settings = self.document.settings
self.settings = settings = self.get_settings()
elements = self.elements = self.default_elements.copy()
elements.update({
# if empty, the title is set to the first section title
@ -243,11 +244,10 @@ class TexinfoTranslator(nodes.NodeVisitor):
language=self.builder.config.language))
})
# title
title = None # type: unicode
title = elements['title'] # type: ignore
title = settings.title # type: unicode
if not title:
title = self.document.next_node(nodes.title)
title = (title and title.astext()) or '<untitled>' # type: ignore
title_node = self.document.next_node(nodes.title)
title = (title and title_node.astext()) or '<untitled>'
elements['title'] = self.escape_id(title) or '<untitled>'
# filename
if not elements['filename']:

View File

@ -20,6 +20,7 @@ from docutils.utils import column_width
from sphinx import addnodes
from sphinx.locale import admonitionlabels, _
from sphinx.util.docutils import SphinxTranslator
if False:
# For type annotation
@ -391,23 +392,23 @@ class TextWriter(writers.Writer):
self.output = cast(TextTranslator, visitor).body
class TextTranslator(nodes.NodeVisitor):
class TextTranslator(SphinxTranslator):
builder = None # type: TextBuilder
def __init__(self, document, builder):
# type: (nodes.document, TextBuilder) -> None
super(TextTranslator, self).__init__(document)
self.builder = builder
super(TextTranslator, self).__init__(builder, document)
newlines = builder.config.text_newlines
newlines = self.config.text_newlines
if newlines == 'windows':
self.nl = '\r\n'
elif newlines == 'native':
self.nl = os.linesep
else:
self.nl = '\n'
self.sectionchars = builder.config.text_sectionchars
self.add_secnumbers = builder.config.text_add_secnumbers
self.secnumber_suffix = builder.config.text_secnumber_suffix
self.sectionchars = self.config.text_sectionchars
self.add_secnumbers = self.config.text_add_secnumbers
self.secnumber_suffix = self.config.text_secnumber_suffix
self.states = [[]] # type: List[List[Tuple[int, Union[unicode, List[unicode]]]]]
self.stateindent = [0]
self.list_counter = [] # type: List[int]

View File

@ -35,6 +35,7 @@ def settings(app):
settings.smart_quotes = True
settings.env = app.builder.env
settings.env.temp_data['docname'] = 'dummy'
settings.contentsname = 'dummy'
domain_context = sphinx_domains(settings.env)
domain_context.enable()
yield settings