Merge branch '2.0' into 6793_sidebar_in_texinfo

This commit is contained in:
Takeshi KOMIYA 2019-11-17 00:26:53 +09:00 committed by GitHub
commit 250ce7425a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 447 additions and 121 deletions

View File

@ -36,7 +36,7 @@ Other contributors, listed alphabetically, are:
* Hernan Grecco -- search improvements * Hernan Grecco -- search improvements
* Horst Gutmann -- internationalization support * Horst Gutmann -- internationalization support
* Martin Hans -- autodoc improvements * Martin Hans -- autodoc improvements
* Zac Hatfield-Dodds -- doctest reporting improvements * Zac Hatfield-Dodds -- doctest reporting improvements, intersphinx performance
* Doug Hellmann -- graphviz improvements * Doug Hellmann -- graphviz improvements
* Tim Hoffmann -- theme improvements * Tim Hoffmann -- theme improvements
* Antti Kaihola -- doctest extension (skipif option) * Antti Kaihola -- doctest extension (skipif option)

20
CHANGES
View File

@ -9,10 +9,13 @@ Incompatible changes
* #6742: ``end-before`` option of :rst:dir:`literalinclude` directive does not * #6742: ``end-before`` option of :rst:dir:`literalinclude` directive does not
match the first line of the code block. match the first line of the code block.
* #1331: Change default User-Agent header to ``"Sphinx/X.Y.Z requests/X.Y.Z
python/X.Y.Z"``. It can be changed via :confval:`user_agent`.
Deprecated Deprecated
---------- ----------
* ``sphinx.builders.gettext.POHEADER``
* ``sphinx.io.SphinxStandaloneReader.app`` * ``sphinx.io.SphinxStandaloneReader.app``
* ``sphinx.io.SphinxStandaloneReader.env`` * ``sphinx.io.SphinxStandaloneReader.env``
@ -23,6 +26,13 @@ Features added
* #267: html: Eliminate prompt characters of doctest block from copyable text * #267: html: Eliminate prompt characters of doctest block from copyable text
* #6729: html theme: agogo theme now supports ``rightsidebar`` option * #6729: html theme: agogo theme now supports ``rightsidebar`` option
* #6780: Add PEP-561 Support * #6780: Add PEP-561 Support
* #6762: latex: Allow to load additonal LaTeX packages via ``extrapackages`` key
of :confval:`latex_elements`
* #1331: Add new config variable: :confval:`user_agent`
* #6000: LaTeX: have backslash also be an inline literal word wrap break
character
* #6812: Improve a warning message when extensions are not parallel safe
* #6818: Improve Intersphinx performance for multiple remote inventories.
Bugs fixed Bugs fixed
---------- ----------
@ -32,6 +42,9 @@ Bugs fixed
.. _latex3/latex2e#173: https://github.com/latex3/latex2e/issues/173 .. _latex3/latex2e#173: https://github.com/latex3/latex2e/issues/173
* #6618: LaTeX: Avoid section names at the end of a page * #6618: LaTeX: Avoid section names at the end of a page
* #6738: LaTeX: Do not replace unicode characters by LaTeX macros on unicode
supported LaTeX engines: ¶, §, €, ∞, ±, →, ‣, , superscript and subscript
digits go through "as is" (as default OpenType font supports them)
* #6704: linkcheck: Be defensive and handle newly defined HTTP error code * #6704: linkcheck: Be defensive and handle newly defined HTTP error code
* #6655: image URLs containing ``data:`` causes gettext builder crashed * #6655: image URLs containing ``data:`` causes gettext builder crashed
* #6584: i18n: Error when compiling message catalogs on Hindi * #6584: i18n: Error when compiling message catalogs on Hindi
@ -42,6 +55,11 @@ Bugs fixed
* #5070: epub: Wrong internal href fragment links * #5070: epub: Wrong internal href fragment links
* #6712: Allow not to install sphinx.testing as runtime (mainly for ALT Linux) * #6712: Allow not to install sphinx.testing as runtime (mainly for ALT Linux)
* #6741: html: search result was broken with empty :confval:`html_file_suffix` * #6741: html: search result was broken with empty :confval:`html_file_suffix`
* #6001: LaTeX does not wrap long code lines at backslash character
* #6804: LaTeX: PDF build breaks if admonition of danger type contains
code-block long enough not to fit on one page
* #6809: LaTeX: code-block in a danger type admonition can easily spill over
bottom of page
* #6793: texinfo: Code examples broken following "sidebar" * #6793: texinfo: Code examples broken following "sidebar"
Testing Testing
@ -65,6 +83,8 @@ Features added
Bugs fixed Bugs fixed
---------- ----------
* #6776: LaTeX: 2019-10-01 LaTeX release breaks :file:`sphinxcyrillic.sty`
Testing Testing
-------- --------

View File

@ -26,6 +26,11 @@ The following is a list of deprecated interfaces.
- (will be) Removed - (will be) Removed
- Alternatives - Alternatives
* - ``sphinx.builders.gettext.POHEADER``
- 2.3
- 4.0
- ``sphinx/templates/gettext/message.pot_t`` (template file)
* - ``sphinx.io.SphinxStandaloneReader.app`` * - ``sphinx.io.SphinxStandaloneReader.app``
- 2.3 - 2.3
- 4.0 - 4.0

View File

@ -226,6 +226,25 @@ into the generated ``.tex`` files. Its ``'sphinxsetup'`` key is described
.. versionadded:: 1.5 .. versionadded:: 1.5
``'extrapackages'``
Additional LaTeX packages. For example:
.. code-block:: python
latex_elements = {
'packages': r'\usepackage{isodate}'
}
It defaults to empty.
The specified LaTeX packages will be loaded before
hyperref package and packages loaded from Sphinx extensions.
.. hint:: If you'd like to load additional LaTeX packages after hyperref, use
``'preamble'`` key instead.
.. versionadded:: 2.3
``'footer'`` ``'footer'``
Additional footer content (before the indices), default empty. Additional footer content (before the indices), default empty.
@ -600,12 +619,15 @@ macros may be significant.
default ``true``. Allows linebreaks inside inline literals: but extra default ``true``. Allows linebreaks inside inline literals: but extra
potential break-points (additionally to those allowed by LaTeX at spaces potential break-points (additionally to those allowed by LaTeX at spaces
or for hyphenation) are currently inserted only after the characters or for hyphenation) are currently inserted only after the characters
``. , ; ? ! /``. Due to TeX internals, white space in the line will be ``. , ; ? ! /`` and ``\``. Due to TeX internals, white space in the line
stretched (or shrunk) in order to accomodate the linebreak. will be stretched (or shrunk) in order to accomodate the linebreak.
.. versionadded:: 1.5 .. versionadded:: 1.5
set this option value to ``false`` to recover former behaviour. set this option value to ``false`` to recover former behaviour.
.. versionchanged:: 2.3.0
added potential breakpoint at ``\`` characters.
``verbatimvisiblespace`` ``verbatimvisiblespace``
default ``\textcolor{red}{\textvisiblespace}``. When a long code line is default ``\textcolor{red}{\textvisiblespace}``. When a long code line is
split, the last space character from the source code line right before the split, the last space character from the source code line right before the

View File

@ -510,6 +510,14 @@ General configuration
.. versionadded:: 1.6.6 .. versionadded:: 1.6.6
.. confval:: user_agent
A User-Agent of Sphinx. It is used for a header on HTTP access (ex.
linkcheck, intersphinx and so on). Default is ``"Sphinx/X.Y.Z
requests/X.Y.Z python/X.Y.Z"``.
.. versionadded:: 2.3
.. confval:: tls_verify .. confval:: tls_verify
If true, Sphinx verifies server certifications. Default is ``True``. If true, Sphinx verifies server certifications. Default is ``True``.

View File

@ -176,6 +176,10 @@ setup(
description='Python documentation generator', description='Python documentation generator',
long_description=long_desc, long_description=long_desc,
long_description_content_type='text/x-rst', long_description_content_type='text/x-rst',
project_urls={
"Code": "https://github.com/sphinx-doc/sphinx",
"Issue tracker": "https://github.com/sphinx-doc/sphinx/issues",
},
zip_safe=False, zip_safe=False,
classifiers=[ classifiers=[
'Development Status :: 5 - Production/Stable', 'Development Status :: 5 - Production/Stable',

View File

@ -1192,26 +1192,30 @@ class Sphinx:
""" """
if typ == 'read': if typ == 'read':
attrname = 'parallel_read_safe' attrname = 'parallel_read_safe'
message = __("the %s extension does not declare if it is safe " message_not_declared = __("the %s extension does not declare if it "
"for parallel reading, assuming it isn't - please " "is safe for parallel reading, assuming "
"ask the extension author to check and make it " "it isn't - please ask the extension author "
"explicit") "to check and make it explicit")
message_not_safe = __("the %s extension is not safe for parallel reading")
elif typ == 'write': elif typ == 'write':
attrname = 'parallel_write_safe' attrname = 'parallel_write_safe'
message = __("the %s extension does not declare if it is safe " message_not_declared = __("the %s extension does not declare if it "
"for parallel writing, assuming it isn't - please " "is safe for parallel writing, assuming "
"ask the extension author to check and make it " "it isn't - please ask the extension author "
"explicit") "to check and make it explicit")
message_not_safe = __("the %s extension is not safe for parallel writing")
else: else:
raise ValueError('parallel type %s is not supported' % typ) raise ValueError('parallel type %s is not supported' % typ)
for ext in self.extensions.values(): for ext in self.extensions.values():
allowed = getattr(ext, attrname, None) allowed = getattr(ext, attrname, None)
if allowed is None: if allowed is None:
logger.warning(message, ext.name) logger.warning(message_not_declared, ext.name)
logger.warning(__('doing serial %s'), typ) logger.warning(__('doing serial %s'), typ)
return False return False
elif not allowed: elif not allowed:
logger.warning(message_not_safe, ext.name)
logger.warning(__('doing serial %s'), typ)
return False return False
return True return True

View File

@ -11,16 +11,16 @@
from codecs import open from codecs import open
from collections import defaultdict, OrderedDict from collections import defaultdict, OrderedDict
from datetime import datetime, tzinfo, timedelta from datetime import datetime, tzinfo, timedelta
from io import StringIO
from os import path, walk, getenv from os import path, walk, getenv
from time import time from time import time
from typing import Any, Dict, Iterable, List, Set, Tuple, Union from typing import Any, Dict, Iterable, Generator, List, Set, Tuple, Union
from uuid import uuid4 from uuid import uuid4
from docutils import nodes from docutils import nodes
from docutils.nodes import Element from docutils.nodes import Element
from sphinx import addnodes from sphinx import addnodes
from sphinx import package_dir
from sphinx.application import Sphinx from sphinx.application import Sphinx
from sphinx.builders import Builder from sphinx.builders import Builder
from sphinx.domains.python import pairindextypes from sphinx.domains.python import pairindextypes
@ -30,8 +30,9 @@ from sphinx.util import split_index_msg, logging, status_iterator
from sphinx.util.console import bold # type: ignore from sphinx.util.console import bold # type: ignore
from sphinx.util.i18n import CatalogInfo, docname_to_domain from sphinx.util.i18n import CatalogInfo, docname_to_domain
from sphinx.util.nodes import extract_messages, traverse_translatable_index from sphinx.util.nodes import extract_messages, traverse_translatable_index
from sphinx.util.osutil import relpath, ensuredir, canon_path from sphinx.util.osutil import ensuredir, canon_path
from sphinx.util.tags import Tags from sphinx.util.tags import Tags
from sphinx.util.template import SphinxRenderer
if False: if False:
# For type annotation # For type annotation
@ -58,7 +59,15 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"""[1:] """[1:] # RemovedInSphinx40Warning
class Message:
"""An entry of translatable message."""
def __init__(self, text: str, locations: List[Tuple[str, int]], uuids: List[str]):
self.text = text
self.locations = locations
self.uuids = uuids
class Catalog: class Catalog:
@ -80,6 +89,12 @@ class Catalog:
self.metadata[msg] = [] self.metadata[msg] = []
self.metadata[msg].append((origin.source, origin.line, origin.uid)) # type: ignore self.metadata[msg].append((origin.source, origin.line, origin.uid)) # type: ignore
def __iter__(self) -> Generator[Message, None, None]:
for message in self.messages:
positions = [(source, line) for source, line, uuid in self.metadata[message]]
uuids = [uuid for source, line, uuid in self.metadata[message]]
yield Message(message, positions, uuids)
class MsgOrigin: class MsgOrigin:
""" """
@ -92,6 +107,22 @@ class MsgOrigin:
self.uid = uuid4().hex self.uid = uuid4().hex
class GettextRenderer(SphinxRenderer):
def __init__(self, template_path: str = None) -> None:
if template_path is None:
template_path = path.join(package_dir, 'templates', 'gettext')
super().__init__(template_path)
def escape(s: str) -> str:
s = s.replace('\\', r'\\')
s = s.replace('"', r'\"')
return s.replace('\n', '\\n"\n"')
# use texescape as escape filter
self.env.filters['e'] = escape
self.env.filters['escape'] = escape
class I18nTags(Tags): class I18nTags(Tags):
"""Dummy tags module for I18nBuilder. """Dummy tags module for I18nBuilder.
@ -247,12 +278,13 @@ class MessageCatalogBuilder(I18nBuilder):
def finish(self) -> None: def finish(self) -> None:
super().finish() super().finish()
data = { context = {
'version': self.config.version, 'version': self.config.version,
'copyright': self.config.copyright, 'copyright': self.config.copyright,
'project': self.config.project, 'project': self.config.project,
'ctime': datetime.fromtimestamp( 'ctime': datetime.fromtimestamp(timestamp, ltz).strftime('%Y-%m-%d %H:%M%z'),
timestamp, ltz).strftime('%Y-%m-%d %H:%M%z'), 'display_location': self.config.gettext_location,
'display_uuid': self.config.gettext_uuid,
} }
for textdomain, catalog in status_iterator(self.catalogs.items(), for textdomain, catalog in status_iterator(self.catalogs.items(),
__("writing message catalogs... "), __("writing message catalogs... "),
@ -262,30 +294,10 @@ class MessageCatalogBuilder(I18nBuilder):
# noop if config.gettext_compact is set # noop if config.gettext_compact is set
ensuredir(path.join(self.outdir, path.dirname(textdomain))) ensuredir(path.join(self.outdir, path.dirname(textdomain)))
context['messages'] = list(catalog)
content = GettextRenderer().render('message.pot_t', context)
pofn = path.join(self.outdir, textdomain + '.pot') pofn = path.join(self.outdir, textdomain + '.pot')
output = StringIO()
output.write(POHEADER % data)
for message in catalog.messages:
positions = catalog.metadata[message]
if self.config.gettext_location:
# generate "#: file1:line1\n#: file2:line2 ..."
output.write("#: %s\n" % "\n#: ".join(
"%s:%s" % (canon_path(relpath(source, self.outdir)), line)
for source, line, _ in positions))
if self.config.gettext_uuid:
# generate "# uuid1\n# uuid2\n ..."
output.write("# %s\n" % "\n# ".join(uid for _, _, uid in positions))
# message contains *one* line of text ready for translation
message = message.replace('\\', r'\\'). \
replace('"', r'\"'). \
replace('\n', '\\n"\n"')
output.write('msgid "%s"\nmsgstr ""\n\n' % message)
content = output.getvalue()
if should_write(pofn, content): if should_write(pofn, content):
with open(pofn, 'w', encoding='utf-8') as pofile: with open(pofn, 'w', encoding='utf-8') as pofile:
pofile.write(content) pofile.write(content)

View File

@ -101,7 +101,6 @@ class CheckExternalLinksBuilder(Builder):
'allow_redirects': True, 'allow_redirects': True,
'headers': { 'headers': {
'Accept': 'text/html,application/xhtml+xml;q=0.9,*/*;q=0.8', 'Accept': 'text/html,application/xhtml+xml;q=0.9,*/*;q=0.8',
'User-Agent': requests.useragent_header[0][1],
}, },
} }
if self.app.config.linkcheck_timeout: if self.app.config.linkcheck_timeout:

View File

@ -148,6 +148,7 @@ class Config:
'math_numfig': (True, 'env', []), 'math_numfig': (True, 'env', []),
'tls_verify': (True, 'env', []), 'tls_verify': (True, 'env', []),
'tls_cacerts': (None, 'env', []), 'tls_cacerts': (None, 'env', []),
'user_agent': (None, 'env', [str]),
'smartquotes': (True, 'env', []), 'smartquotes': (True, 'env', []),
'smartquotes_action': ('qDe', 'env', []), 'smartquotes_action': ('qDe', 'env', []),
'smartquotes_excludes': ({'languages': ['ja'], 'smartquotes_excludes': ({'languages': ['ja'],

View File

@ -23,6 +23,7 @@
:license: BSD, see LICENSE for details. :license: BSD, see LICENSE for details.
""" """
import concurrent.futures
import functools import functools
import posixpath import posixpath
import sys import sys
@ -187,21 +188,18 @@ def fetch_inventory(app: Sphinx, uri: str, inv: Any) -> Any:
return invdata return invdata
def load_mappings(app: Sphinx) -> None: def fetch_inventory_group(
"""Load all intersphinx mappings into the environment.""" name: str, uri: str, invs: Any, cache: Any, app: Any, now: float
now = int(time.time()) ) -> bool:
cache_time = now - app.config.intersphinx_cache_limit * 86400 cache_time = now - app.config.intersphinx_cache_limit * 86400
inventories = InventoryAdapter(app.builder.env) failures = []
update = False try:
for key, (name, (uri, invs)) in app.config.intersphinx_mapping.items():
failures = []
for inv in invs: for inv in invs:
if not inv: if not inv:
inv = posixpath.join(uri, INVENTORY_FILENAME) inv = posixpath.join(uri, INVENTORY_FILENAME)
# decide whether the inventory must be read: always read local # decide whether the inventory must be read: always read local
# files; remote ones only if the cache time is expired # files; remote ones only if the cache time is expired
if '://' not in inv or uri not in inventories.cache \ if '://' not in inv or uri not in cache or cache[uri][1] < cache_time:
or inventories.cache[uri][1] < cache_time:
safe_inv_url = _get_safe_url(inv) safe_inv_url = _get_safe_url(inv)
logger.info(__('loading intersphinx inventory from %s...'), safe_inv_url) logger.info(__('loading intersphinx inventory from %s...'), safe_inv_url)
try: try:
@ -209,12 +207,11 @@ def load_mappings(app: Sphinx) -> None:
except Exception as err: except Exception as err:
failures.append(err.args) failures.append(err.args)
continue continue
if invdata: if invdata:
inventories.cache[uri] = (name, now, invdata) cache[uri] = (name, now, invdata)
update = True return True
break return False
finally:
if failures == []: if failures == []:
pass pass
elif len(failures) < len(invs): elif len(failures) < len(invs):
@ -227,7 +224,21 @@ def load_mappings(app: Sphinx) -> None:
logger.warning(__("failed to reach any of the inventories " logger.warning(__("failed to reach any of the inventories "
"with the following issues:") + "\n" + issues) "with the following issues:") + "\n" + issues)
if update:
def load_mappings(app: Sphinx) -> None:
"""Load all intersphinx mappings into the environment."""
now = int(time.time())
inventories = InventoryAdapter(app.builder.env)
with concurrent.futures.ThreadPoolExecutor() as pool:
futures = []
for name, (uri, invs) in app.config.intersphinx_mapping.values():
futures.append(pool.submit(
fetch_inventory_group, name, uri, invs, inventories.cache, app, now
))
updated = [f.result() for f in concurrent.futures.as_completed(futures)]
if any(updated):
inventories.clear() inventories.clear()
# Duplicate values in different inventories will shadow each # Duplicate values in different inventories will shadow each
@ -374,6 +385,7 @@ def inspect_main(argv: List[str]) -> None:
class MockConfig: class MockConfig:
intersphinx_timeout = None # type: int intersphinx_timeout = None # type: int
tls_verify = False tls_verify = False
user_agent = None
class MockApp: class MockApp:
srcdir = '' srcdir = ''

View File

@ -30,7 +30,7 @@ from sphinx.locale import _, __
from sphinx.util import logging from sphinx.util import logging
from sphinx.util.docutils import SphinxDirective from sphinx.util.docutils import SphinxDirective
from sphinx.util.nodes import make_refnode from sphinx.util.nodes import make_refnode
from sphinx.util.texescape import tex_escape_map from sphinx.util.texescape import get_escape_func
from sphinx.writers.html import HTMLTranslator from sphinx.writers.html import HTMLTranslator
from sphinx.writers.latex import LaTeXTranslator from sphinx.writers.latex import LaTeXTranslator
@ -299,10 +299,11 @@ def depart_todo_node(self: HTMLTranslator, node: todo_node) -> None:
def latex_visit_todo_node(self: LaTeXTranslator, node: todo_node) -> None: def latex_visit_todo_node(self: LaTeXTranslator, node: todo_node) -> None:
if self.config.todo_include_todos: if self.config.todo_include_todos:
escape = get_escape_func(self.config.latex_engine)
self.body.append('\n\\begin{sphinxadmonition}{note}{') self.body.append('\n\\begin{sphinxadmonition}{note}{')
self.body.append(self.hypertarget_to(node)) self.body.append(self.hypertarget_to(node))
title_node = cast(nodes.title, node[0]) title_node = cast(nodes.title, node[0])
self.body.append('%s:}' % title_node.astext().translate(tex_escape_map)) self.body.append('%s:}' % escape(title_node.astext()))
node.pop(0) node.pop(0)
else: else:
raise nodes.SkipNode raise nodes.SkipNode

View File

@ -28,7 +28,7 @@ from sphinx.ext import doctest
from sphinx.locale import __ from sphinx.locale import __
from sphinx.pygments_styles import SphinxStyle, NoneStyle from sphinx.pygments_styles import SphinxStyle, NoneStyle
from sphinx.util import logging from sphinx.util import logging
from sphinx.util.texescape import tex_hl_escape_map_new from sphinx.util.texescape import get_hlescape_func, tex_hl_escape_map_new
if False: if False:
# For type annotation # For type annotation
@ -68,9 +68,11 @@ class PygmentsBridge:
html_formatter = HtmlFormatter html_formatter = HtmlFormatter
latex_formatter = LatexFormatter latex_formatter = LatexFormatter
def __init__(self, dest='html', stylename='sphinx', trim_doctest_flags=None): def __init__(self, dest='html', stylename='sphinx', trim_doctest_flags=None,
# type: (str, str, bool) -> None latex_engine=None):
# type: (str, str, bool, str) -> None
self.dest = dest self.dest = dest
self.latex_engine = latex_engine
style = self.get_style(stylename) style = self.get_style(stylename)
self.formatter_args = {'style': style} # type: Dict[str, Any] self.formatter_args = {'style': style} # type: Dict[str, Any]
@ -192,7 +194,8 @@ class PygmentsBridge:
if self.dest == 'html': if self.dest == 'html':
return hlsource return hlsource
else: else:
return hlsource.translate(tex_hl_escape_map_new) escape = get_hlescape_func(self.latex_engine)
return escape(hlsource)
def get_stylesheet(self): def get_stylesheet(self):
# type: () -> str # type: () -> str

View File

@ -0,0 +1,33 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) {{ copyright }}
# This file is distributed under the same license as the {{ project }} package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: {{ project|e }} {{ version|e }}\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: {{ ctime|e }}\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
{% for message in messages %}
{% if display_location -%}
{% for source, line in message.locations -%}
#: {{ source }}:{{ line }}
{% endfor -%}
{% endif -%}
{% if display_uuid -%}
{% for uuid in message.uuids -%}
#: {{ uuid }}
{% endfor -%}
{% endif -%}
msgid "{{ message.text|e }}"
msgstr ""
{% endfor -%}

View File

@ -35,6 +35,7 @@
<%= sphinxsetup %> <%= sphinxsetup %>
<%= fvset %> <%= fvset %>
<%= geometry %> <%= geometry %>
<%= extrapackages %>
<%- for name, option in packages %> <%- for name, option in packages %>
<%- if option %> <%- if option %>

View File

@ -1054,7 +1054,7 @@
% Take advantage of the already applied Pygments mark-up to insert % Take advantage of the already applied Pygments mark-up to insert
% potential linebreaks for TeX processing. % potential linebreaks for TeX processing.
% {, <, #, %, $, ' and ": go to next line. % {, <, #, %, $, ' and ": go to next line.
% _, }, ^, &, >, - and ~: stay at end of broken line. % _, }, ^, &, >, -, ~, and \: stay at end of broken line.
% Use of \textquotesingle for straight quote. % Use of \textquotesingle for straight quote.
% FIXME: convert this to package options ? % FIXME: convert this to package options ?
\newcommand*\sphinxbreaksbeforelist {% \newcommand*\sphinxbreaksbeforelist {%
@ -1066,6 +1066,7 @@
\newcommand*\sphinxbreaksafterlist {% \newcommand*\sphinxbreaksafterlist {%
\do\PYGZus\_\do\PYGZcb\}\do\PYGZca\^\do\PYGZam\&% _, }, ^, &, \do\PYGZus\_\do\PYGZcb\}\do\PYGZca\^\do\PYGZam\&% _, }, ^, &,
\do\PYGZgt\>\do\PYGZhy\-\do\PYGZti\~% >, -, ~ \do\PYGZgt\>\do\PYGZhy\-\do\PYGZti\~% >, -, ~
\do\PYGZbs\\% \
} }
\newcommand*\sphinxbreaksatspecials {% \newcommand*\sphinxbreaksatspecials {%
\def\do##1##2% \def\do##1##2%
@ -1110,6 +1111,9 @@
\newcommand*\sphinxVerbatimTitle {} \newcommand*\sphinxVerbatimTitle {}
% This box to typeset the caption before framed.sty multiple passes for framing. % This box to typeset the caption before framed.sty multiple passes for framing.
\newbox\sphinxVerbatim@TitleBox \newbox\sphinxVerbatim@TitleBox
% This box to measure contents if nested as inner \MakeFramed requires then
% minipage encapsulation but too long contents then break outer \MakeFramed
\newbox\sphinxVerbatim@ContentsBox
% This is a workaround to a "feature" of French lists, when literal block % This is a workaround to a "feature" of French lists, when literal block
% follows immediately; usable generally (does only \par then), a priori... % follows immediately; usable generally (does only \par then), a priori...
\newcommand*\sphinxvspacefixafterfrenchlists{% \newcommand*\sphinxvspacefixafterfrenchlists{%
@ -1256,17 +1260,23 @@
\itemsep \z@skip \itemsep \z@skip
\topsep \z@skip \topsep \z@skip
\partopsep \z@skip \partopsep \z@skip
% trivlist will set \parsep to \parskip = zero % trivlist will set \parsep to \parskip (which itself is set to zero above)
% \leftmargin will be set to zero by trivlist % \leftmargin will be set to zero by trivlist
\rightmargin\z@ \rightmargin\z@
\parindent \z@% becomes \itemindent. Default zero, but perhaps overwritten. \parindent \z@% becomes \itemindent. Default zero, but perhaps overwritten.
\trivlist\item\relax \trivlist\item\relax
\ifsphinxverbatimwithminipage\spx@inframedtrue\fi \ifspx@inframed\setbox\sphinxVerbatim@ContentsBox\vbox\bgroup
% use a minipage if we are already inside a framed environment \@setminipage\hsize\linewidth
\ifspx@inframed\noindent\begin{minipage}{\linewidth}\fi % use bulk of minipage paragraph shape restores (this is needed
\MakeFramed {% adapted over from framed.sty's snugshade environment % in indented contexts, at least for some)
\textwidth\hsize \columnwidth\hsize \@totalleftmargin\z@
\leftskip\z@skip \rightskip\z@skip \@rightskip\z@skip
\else
\ifsphinxverbatimwithminipage\noindent\begin{minipage}{\linewidth}\fi
\MakeFramed {% adapted over from framed.sty's snugshade environment
\advance\hsize-\width\@totalleftmargin\z@\linewidth\hsize\@setminipage \advance\hsize-\width\@totalleftmargin\z@\linewidth\hsize\@setminipage
}% }%
\fi
% For grid placement from \strut's in \FancyVerbFormatLine % For grid placement from \strut's in \FancyVerbFormatLine
\lineskip\z@skip \lineskip\z@skip
% active comma should not be overwritten by \@noligs % active comma should not be overwritten by \@noligs
@ -1278,8 +1288,49 @@
} }
{% {%
\endOriginalVerbatim \endOriginalVerbatim
\par\unskip\@minipagefalse\endMakeFramed % from framed.sty snugshade \ifspx@inframed
\ifspx@inframed\end{minipage}\fi \egroup % finish \sphinxVerbatim@ContentsBox vbox
\nobreak % update page totals
\ifdim\dimexpr\ht\sphinxVerbatim@ContentsBox+
\dp\sphinxVerbatim@ContentsBox+
\ht\sphinxVerbatim@TitleBox+
\dp\sphinxVerbatim@TitleBox+
2\fboxsep+2\fboxrule+
% try to account for external frame parameters
\FrameSep+\FrameRule+
% Usage here of 2 baseline distances is empirical.
% In border case where code-block fits barely in remaining space,
% it gets framed and looks good but the outer frame may continue
% on top of next page and give (if no contents after code-block)
% an empty framed line, as testing showed.
2\baselineskip+
% now add all to accumulated page totals and compare to \pagegoal
\pagetotal+\pagedepth>\pagegoal
% long contents: do not \MakeFramed. Do make a caption (either before or
% after) if title exists. Continuation hints across pagebreaks dropped.
% FIXME? a bottom caption may end up isolated at top of next page
% (no problem with a top caption, which is default)
\spx@opt@verbatimwithframefalse
\def\sphinxVerbatim@Title{\noindent\box\sphinxVerbatim@TitleBox\par}%
\sphinxVerbatim@Before
\noindent\unvbox\sphinxVerbatim@ContentsBox\par
\sphinxVerbatim@After
\else
% short enough contents: use \MakeFramed. As it is nested, this requires
% minipage encapsulation.
\noindent\begin{minipage}{\linewidth}%
\MakeFramed {% Use it now with the fetched contents
\advance\hsize-\width\@totalleftmargin\z@\linewidth\hsize\@setminipage
}%
\unvbox\sphinxVerbatim@ContentsBox
% some of this may be superfluous:
\par\unskip\@minipagefalse\endMakeFramed
\end{minipage}%
\fi
\else % non-nested \MakeFramed
\par\unskip\@minipagefalse\endMakeFramed % from framed.sty snugshade
\ifsphinxverbatimwithminipage\end{minipage}\fi
\fi
\endtrivlist \endtrivlist
} }
\newenvironment {sphinxVerbatimNoFrame} \newenvironment {sphinxVerbatimNoFrame}
@ -1314,6 +1365,7 @@
{\def##1{\discretionary{\char`##2}{\sphinxafterbreak}{\char`##2}}}% {\def##1{\discretionary{\char`##2}{\sphinxafterbreak}{\char`##2}}}%
\do\_\_\do\}\}\do\textasciicircum\^\do\&\&% _, }, ^, &, \do\_\_\do\}\}\do\textasciicircum\^\do\&\&% _, }, ^, &,
\do\textgreater\>\do\textasciitilde\~% >, ~ \do\textgreater\>\do\textasciitilde\~% >, ~
\do\textbackslash\\% \
} }
\newcommand*\sphinxbreaksviaactiveinparsedliteral{% \newcommand*\sphinxbreaksviaactiveinparsedliteral{%
\sphinxbreaksviaactive % by default handles . , ; ? ! / \sphinxbreaksviaactive % by default handles . , ; ? ! /
@ -1736,13 +1788,25 @@
% to obtain straight quotes we execute \@noligs as patched by upquote, and % to obtain straight quotes we execute \@noligs as patched by upquote, and
% \scantokens is needed in cases where it would be too late for the macro to % \scantokens is needed in cases where it would be too late for the macro to
% first set catcodes and then fetch its argument. We also make the contents % first set catcodes and then fetch its argument. We also make the contents
% breakable at non-escaped . , ; ? ! / using \sphinxbreaksviaactive. % breakable at non-escaped . , ; ? ! / using \sphinxbreaksviaactive,
% and also at \ character (which is escaped to \textbackslash{}).
\protected\def\sphinxtextbackslashbreakbefore
{\discretionary{}{\sphinxafterbreak\sphinx@textbackslash}{\sphinx@textbackslash}}
\protected\def\sphinxtextbackslashbreakafter
{\discretionary{\sphinx@textbackslash}{\sphinxafterbreak}{\sphinx@textbackslash}}
\let\sphinxtextbackslash\sphinxtextbackslashbreakafter
% the macro must be protected if it ends up used in moving arguments, % the macro must be protected if it ends up used in moving arguments,
% in 'alltt' \@noligs is done already, and the \scantokens must be avoided. % in 'alltt' \@noligs is done already, and the \scantokens must be avoided.
\protected\def\sphinxupquote#1{{\def\@tempa{alltt}% \protected\def\sphinxupquote#1{{\def\@tempa{alltt}%
\ifx\@tempa\@currenvir\else \ifx\@tempa\@currenvir\else
\ifspx@opt@inlineliteralwraps \ifspx@opt@inlineliteralwraps
\sphinxbreaksviaactive\let\sphinxafterbreak\empty % break at . , ; ? ! /
\sphinxbreaksviaactive
% break also at \
\let\sphinx@textbackslash\textbackslash
\let\textbackslash\sphinxtextbackslash
% do not typeset a continuation symbol on next line
\let\sphinxafterbreak\sphinxafterbreakofinlineliteral
% do not overwrite the comma set-up % do not overwrite the comma set-up
\let\verbatim@nolig@list\sphinx@literal@nolig@list \let\verbatim@nolig@list\sphinx@literal@nolig@list
\fi \fi
@ -1754,6 +1818,7 @@
\def\sphinx@do@noligs #1{\catcode`#1\active\begingroup\lccode`\~`#1\relax \def\sphinx@do@noligs #1{\catcode`#1\active\begingroup\lccode`\~`#1\relax
\lowercase{\endgroup\def~{\leavevmode\kern\z@\char`#1 }}} \lowercase{\endgroup\def~{\leavevmode\kern\z@\char`#1 }}}
\def\sphinx@literal@nolig@list {\do\`\do\<\do\>\do\'\do\-}% \def\sphinx@literal@nolig@list {\do\`\do\<\do\>\do\'\do\-}%
\let\sphinxafterbreakofinlineliteral\empty
% Some custom font markup commands. % Some custom font markup commands.
\protected\def\sphinxstrong#1{\textbf{#1}} \protected\def\sphinxstrong#1{\textbf{#1}}

View File

@ -11,7 +11,7 @@
\ProcessLocalKeyvalOptions* % ignore class options \ProcessLocalKeyvalOptions* % ignore class options
\ifspx@cyropt@Xtwo \ifspx@cyropt@Xtwo
% original code by tex.sx user egreg: % original code by tex.sx user egreg (updated 2019/10/28):
% https://tex.stackexchange.com/a/460325/ % https://tex.stackexchange.com/a/460325/
% 159 Cyrillic glyphs as available in X2 TeX 8bit font encoding % 159 Cyrillic glyphs as available in X2 TeX 8bit font encoding
% This assumes inputenc loaded with utf8 option, or LaTeX release % This assumes inputenc loaded with utf8 option, or LaTeX release
@ -27,7 +27,9 @@
{Ӎ}{ӎ}{Ӕ}{ӕ}{Ә}{ә}{Ӡ}{ӡ}{Ө}{ө}\do {Ӎ}{ӎ}{Ӕ}{ӕ}{Ә}{ә}{Ӡ}{ӡ}{Ө}{ө}\do
{% {%
\begingroup\def\IeC{\protect\DeclareTextSymbolDefault}% \begingroup\def\IeC{\protect\DeclareTextSymbolDefault}%
\protected@edef\@temp{\endgroup\next{X2}}\@temp \protected@edef\@temp{\endgroup
\@ifl@t@r{\fmtversion}{2019/10/01}{\csname u8:\next\endcsname}{\next}}%
\@temp{X2}%
}% }%
\else \else
\ifspx@cyropt@TtwoA \ifspx@cyropt@TtwoA

View File

@ -187,9 +187,8 @@ class sphinx_domains:
def __enter__(self) -> None: def __enter__(self) -> None:
self.enable() self.enable()
def __exit__(self, exc_type: "Type[Exception]", exc_value: Exception, traceback: Any) -> bool: # type: ignore # NOQA def __exit__(self, exc_type: "Type[Exception]", exc_value: Exception, traceback: Any) -> None: # NOQA
self.disable() self.disable()
return False
def enable(self) -> None: def enable(self) -> None:
self.directive_func = directives.directive self.directive_func = directives.directive

View File

@ -8,6 +8,7 @@
:license: BSD, see LICENSE for details. :license: BSD, see LICENSE for details.
""" """
import sys
import warnings import warnings
from contextlib import contextmanager from contextlib import contextmanager
from typing import Generator, Union from typing import Generator, Union
@ -16,6 +17,7 @@ from urllib.parse import urlsplit
import pkg_resources import pkg_resources
import requests import requests
import sphinx
from sphinx.config import Config from sphinx.config import Config
try: try:
@ -105,14 +107,28 @@ def _get_tls_cacert(url: str, config: Config) -> Union[str, bool]:
return certs.get(hostname, True) return certs.get(hostname, True)
def _get_user_agent(config: Config) -> str:
if config.user_agent:
return config.user_agent
else:
return ' '.join([
'Sphinx/%s' % sphinx.__version__,
'requests/%s' % requests.__version__,
'python/%s' % '.'.join(map(str, sys.version_info[:3])),
])
def get(url: str, **kwargs) -> requests.Response: def get(url: str, **kwargs) -> requests.Response:
"""Sends a GET request like requests.get(). """Sends a GET request like requests.get().
This sets up User-Agent header and TLS verification automatically.""" This sets up User-Agent header and TLS verification automatically."""
kwargs.setdefault('headers', dict(useragent_header)) headers = kwargs.setdefault('headers', {})
config = kwargs.pop('config', None) config = kwargs.pop('config', None)
if config: if config:
kwargs.setdefault('verify', _get_tls_cacert(url, config)) kwargs.setdefault('verify', _get_tls_cacert(url, config))
headers.setdefault('User-Agent', _get_user_agent(config))
else:
headers.setdefault('User-Agent', useragent_header[0][1])
with ignore_insecure_warning(**kwargs): with ignore_insecure_warning(**kwargs):
return requests.get(url, **kwargs) return requests.get(url, **kwargs)
@ -122,10 +138,13 @@ def head(url: str, **kwargs) -> requests.Response:
"""Sends a HEAD request like requests.head(). """Sends a HEAD request like requests.head().
This sets up User-Agent header and TLS verification automatically.""" This sets up User-Agent header and TLS verification automatically."""
kwargs.setdefault('headers', dict(useragent_header)) headers = kwargs.setdefault('headers', {})
config = kwargs.pop('config', None) config = kwargs.pop('config', None)
if config: if config:
kwargs.setdefault('verify', _get_tls_cacert(url, config)) kwargs.setdefault('verify', _get_tls_cacert(url, config))
headers.setdefault('User-Agent', _get_user_agent(config))
else:
headers.setdefault('User-Agent', useragent_header[0][1])
with ignore_insecure_warning(**kwargs): with ignore_insecure_warning(**kwargs):
return requests.get(url, **kwargs) return requests.get(url, **kwargs)

View File

@ -63,14 +63,14 @@ class SphinxRenderer(FileRenderer):
class LaTeXRenderer(SphinxRenderer): class LaTeXRenderer(SphinxRenderer):
def __init__(self, template_path: str = None) -> None: def __init__(self, template_path: str = None, latex_engine: str = None) -> None:
if template_path is None: if template_path is None:
template_path = os.path.join(package_dir, 'templates', 'latex') template_path = os.path.join(package_dir, 'templates', 'latex')
super().__init__(template_path) super().__init__(template_path)
# use texescape as escape filter # use texescape as escape filter
self.env.filters['e'] = texescape.escape self.env.filters['e'] = texescape.get_escape_func(latex_engine)
self.env.filters['escape'] = texescape.escape self.env.filters['escape'] = texescape.get_escape_func(latex_engine)
self.env.filters['eabbr'] = texescape.escape_abbr self.env.filters['eabbr'] = texescape.escape_abbr
# use JSP/eRuby like tagging instead because curly bracket; the default # use JSP/eRuby like tagging instead because curly bracket; the default

View File

@ -9,7 +9,7 @@
""" """
import re import re
from typing import Dict from typing import Callable, Dict
tex_replacements = [ tex_replacements = [
# map TeX special chars # map TeX special chars
@ -20,15 +20,38 @@ tex_replacements = [
('_', r'\_'), ('_', r'\_'),
('{', r'\{'), ('{', r'\{'),
('}', r'\}'), ('}', r'\}'),
('[', r'{[}'),
(']', r'{]}'),
('`', r'{}`'),
('\\', r'\textbackslash{}'), ('\\', r'\textbackslash{}'),
('~', r'\textasciitilde{}'), ('~', r'\textasciitilde{}'),
('^', r'\textasciicircum{}'),
# map chars to avoid mis-interpretation in LaTeX
('[', r'{[}'),
(']', r'{]}'),
# map chars to avoid TeX ligatures
# 1. ' - and , not here for some legacy reason
# 2. no effect with lualatex (done otherwise: #5790)
('`', r'{}`'),
('<', r'\textless{}'), ('<', r'\textless{}'),
('>', r'\textgreater{}'), ('>', r'\textgreater{}'),
('^', r'\textasciicircum{}'), # map char for some unknown reason. TODO: remove this?
('|', r'\textbar{}'),
# map special Unicode characters to TeX commands # map special Unicode characters to TeX commands
('', r'\(\checkmark\)'),
('', r'\(\pmb{\checkmark}\)'),
# used to separate -- in options
('', r'{}'),
# map some special Unicode characters to similar ASCII ones
# (even for Unicode LaTeX as may not be supported by OpenType font)
('', r'\_'),
('', r'e'),
('', r'i'),
# Greek alphabet not escaped: pdflatex handles it via textalpha and inputenc
# OHM SIGN U+2126 is handled by LaTeX textcomp package
]
# A map Unicode characters to LaTeX representation
# (for LaTeX engines which don't support unicode)
unicode_tex_replacements = [
# map some more common Unicode characters to TeX commands
('', r'\P{}'), ('', r'\P{}'),
('§', r'\S{}'), ('§', r'\S{}'),
('', r'\texteuro{}'), ('', r'\texteuro{}'),
@ -36,16 +59,8 @@ tex_replacements = [
('±', r'\(\pm\)'), ('±', r'\(\pm\)'),
('', r'\(\rightarrow\)'), ('', r'\(\rightarrow\)'),
('', r'\(\rightarrow\)'), ('', r'\(\rightarrow\)'),
('', r'\(\checkmark\)'),
('', r'\(\pmb{\checkmark}\)'),
# used to separate -- in options
('', r'{}'),
# map some special Unicode characters to similar ASCII ones
('', r'\_'),
('', r'\textendash{}'), ('', r'\textendash{}'),
('|', r'\textbar{}'), # superscript
('', r'e'),
('', r'i'),
('', r'\(\sp{\text{0}}\)'), ('', r'\(\sp{\text{0}}\)'),
('¹', r'\(\sp{\text{1}}\)'), ('¹', r'\(\sp{\text{1}}\)'),
('²', r'\(\sp{\text{2}}\)'), ('²', r'\(\sp{\text{2}}\)'),
@ -56,6 +71,7 @@ tex_replacements = [
('', r'\(\sp{\text{7}}\)'), ('', r'\(\sp{\text{7}}\)'),
('', r'\(\sp{\text{8}}\)'), ('', r'\(\sp{\text{8}}\)'),
('', r'\(\sp{\text{9}}\)'), ('', r'\(\sp{\text{9}}\)'),
# subscript
('', r'\(\sb{\text{0}}\)'), ('', r'\(\sb{\text{0}}\)'),
('', r'\(\sb{\text{1}}\)'), ('', r'\(\sb{\text{1}}\)'),
('', r'\(\sb{\text{2}}\)'), ('', r'\(\sb{\text{2}}\)'),
@ -66,13 +82,21 @@ tex_replacements = [
('', r'\(\sb{\text{7}}\)'), ('', r'\(\sb{\text{7}}\)'),
('', r'\(\sb{\text{8}}\)'), ('', r'\(\sb{\text{8}}\)'),
('', r'\(\sb{\text{9}}\)'), ('', r'\(\sb{\text{9}}\)'),
# Greek alphabet not escaped: pdflatex handles it via textalpha and inputenc
# OHM SIGN U+2126 is handled by LaTeX textcomp package
] ]
tex_escape_map = {} # type: Dict[int, str] tex_escape_map = {} # type: Dict[int, str]
tex_escape_map_without_unicode = {} # type: Dict[int, str]
tex_replace_map = {} tex_replace_map = {}
tex_hl_escape_map_new = {} tex_hl_escape_map_new = {} # type: Dict[int, str]
tex_hl_escape_map_new_without_unicode = {} # type: Dict[int, str]
def get_escape_func(latex_engine: str) -> Callable[[str], str]:
"""Get escape() function for given latex_engine."""
if latex_engine in ('lualatex', 'xelatex'):
return escape_for_unicode_latex_engine
else:
return escape
def escape(s: str) -> str: def escape(s: str) -> str:
@ -80,6 +104,29 @@ def escape(s: str) -> str:
return s.translate(tex_escape_map) return s.translate(tex_escape_map)
def escape_for_unicode_latex_engine(s: str) -> str:
"""Escape text for unicode supporting LaTeX engine."""
return s.translate(tex_escape_map_without_unicode)
def get_hlescape_func(latex_engine: str) -> Callable[[str], str]:
"""Get hlescape() function for given latex_engine."""
if latex_engine in ('lualatex', 'xelatex'):
return hlescape_for_unicode_latex_engine
else:
return hlescape
def hlescape(s: str) -> str:
"""Escape text for LaTeX highlighter."""
return s.translate(tex_hl_escape_map_new)
def hlescape_for_unicode_latex_engine(s: str) -> str:
"""Escape text for unicode supporting LaTeX engine."""
return s.translate(tex_hl_escape_map_new_without_unicode)
def escape_abbr(text: str) -> str: def escape_abbr(text: str) -> str:
"""Adjust spacing after abbreviations. Works with @ letter or other.""" """Adjust spacing after abbreviations. Works with @ letter or other."""
return re.sub(r'\.(?=\s|$)', r'.\@{}', text) return re.sub(r'\.(?=\s|$)', r'.\@{}', text)
@ -87,6 +134,11 @@ def escape_abbr(text: str) -> str:
def init() -> None: def init() -> None:
for a, b in tex_replacements: for a, b in tex_replacements:
tex_escape_map[ord(a)] = b
tex_escape_map_without_unicode[ord(a)] = b
tex_replace_map[ord(a)] = '_'
for a, b in unicode_tex_replacements:
tex_escape_map[ord(a)] = b tex_escape_map[ord(a)] = b
tex_replace_map[ord(a)] = '_' tex_replace_map[ord(a)] = '_'
@ -94,3 +146,7 @@ def init() -> None:
if a in '[]{}\\': if a in '[]{}\\':
continue continue
tex_hl_escape_map_new[ord(a)] = b tex_hl_escape_map_new[ord(a)] = b
tex_hl_escape_map_new_without_unicode[ord(a)] = b
for a, b in unicode_tex_replacements:
tex_hl_escape_map_new[ord(a)] = b

View File

@ -32,7 +32,7 @@ from sphinx.util import split_into, logging
from sphinx.util.docutils import SphinxTranslator from sphinx.util.docutils import SphinxTranslator
from sphinx.util.nodes import clean_astext, get_prev_node from sphinx.util.nodes import clean_astext, get_prev_node
from sphinx.util.template import LaTeXRenderer from sphinx.util.template import LaTeXRenderer
from sphinx.util.texescape import tex_escape_map, tex_replace_map from sphinx.util.texescape import get_escape_func, tex_replace_map
try: try:
from docutils.utils.roman import toRoman from docutils.utils.roman import toRoman
@ -155,6 +155,7 @@ DEFAULT_SETTINGS = {
'% Set up styles of URL: it should be placed after hyperref.\n' '% Set up styles of URL: it should be placed after hyperref.\n'
'\\urlstyle{same}'), '\\urlstyle{same}'),
'contentsname': '', 'contentsname': '',
'extrapackages': '',
'preamble': '', 'preamble': '',
'title': '', 'title': '',
'release': '', 'release': '',
@ -499,6 +500,9 @@ class LaTeXTranslator(SphinxTranslator):
self.compact_list = 0 self.compact_list = 0
self.first_param = 0 self.first_param = 0
# escape helper
self.escape = get_escape_func(self.config.latex_engine)
# sort out some elements # sort out some elements
self.elements = self.builder.context.copy() self.elements = self.builder.context.copy()
@ -649,7 +653,8 @@ class LaTeXTranslator(SphinxTranslator):
self.elements['classoptions'] += ',' + \ self.elements['classoptions'] += ',' + \
self.elements['extraclassoptions'] self.elements['extraclassoptions']
self.highlighter = highlighting.PygmentsBridge('latex', self.config.pygments_style) self.highlighter = highlighting.PygmentsBridge('latex', self.config.pygments_style,
latex_engine=self.config.latex_engine)
self.context = [] # type: List[Any] self.context = [] # type: List[Any]
self.descstack = [] # type: List[str] self.descstack = [] # type: List[str]
self.table = None # type: Table self.table = None # type: Table
@ -757,8 +762,7 @@ class LaTeXTranslator(SphinxTranslator):
for i, (letter, entries) in enumerate(content): for i, (letter, entries) in enumerate(content):
if i > 0: if i > 0:
ret.append('\\indexspace\n') ret.append('\\indexspace\n')
ret.append('\\bigletter{%s}\n' % ret.append('\\bigletter{%s}\n' % self.escape(letter))
str(letter).translate(tex_escape_map))
for entry in entries: for entry in entries:
if not entry[3]: if not entry[3]:
continue continue
@ -793,13 +797,14 @@ class LaTeXTranslator(SphinxTranslator):
def render(self, template_name, variables): def render(self, template_name, variables):
# type: (str, Dict) -> str # type: (str, Dict) -> str
renderer = LaTeXRenderer(latex_engine=self.config.latex_engine)
for template_dir in self.builder.config.templates_path: for template_dir in self.builder.config.templates_path:
template = path.join(self.builder.confdir, template_dir, template = path.join(self.builder.confdir, template_dir,
template_name) template_name)
if path.exists(template): if path.exists(template):
return LaTeXRenderer().render(template, variables) return renderer.render(template, variables)
return LaTeXRenderer().render(template_name, variables) return renderer.render(template_name, variables)
def visit_document(self, node): def visit_document(self, node):
# type: (nodes.Element) -> None # type: (nodes.Element) -> None
@ -913,14 +918,13 @@ class LaTeXTranslator(SphinxTranslator):
if not self.elements['title']: if not self.elements['title']:
# text needs to be escaped since it is inserted into # text needs to be escaped since it is inserted into
# the output literally # the output literally
self.elements['title'] = node.astext().translate(tex_escape_map) self.elements['title'] = self.escape(node.astext())
self.this_is_the_title = 0 self.this_is_the_title = 0
raise nodes.SkipNode raise nodes.SkipNode
else: else:
short = '' short = ''
if node.traverse(nodes.image): if node.traverse(nodes.image):
short = ('[%s]' % short = ('[%s]' % self.escape(' '.join(clean_astext(node).split())))
' '.join(clean_astext(node).split()).translate(tex_escape_map))
try: try:
self.body.append(r'\%s%s{' % (self.sectionnames[self.sectionlevel], short)) self.body.append(r'\%s%s{' % (self.sectionnames[self.sectionlevel], short))
@ -1954,8 +1958,7 @@ class LaTeXTranslator(SphinxTranslator):
else: else:
id = node.get('refuri', '')[1:].replace('#', ':') id = node.get('refuri', '')[1:].replace('#', ':')
title = node.get('title', '%s') title = self.escape(node.get('title', '%s')).replace('\\%s', '%s')
title = str(title).translate(tex_escape_map).replace('\\%s', '%s')
if '\\{name\\}' in title or '\\{number\\}' in title: if '\\{name\\}' in title or '\\{number\\}' in title:
# new style format (cf. "Fig.%{number}") # new style format (cf. "Fig.%{number}")
title = title.replace('\\{name\\}', '{name}').replace('\\{number\\}', '{number}') title = title.replace('\\{name\\}', '{name}').replace('\\{number\\}', '{number}')
@ -2403,7 +2406,7 @@ class LaTeXTranslator(SphinxTranslator):
def encode(self, text): def encode(self, text):
# type: (str) -> str # type: (str) -> str
text = str(text).translate(tex_escape_map) text = self.escape(text)
if self.literal_whitespace: if self.literal_whitespace:
# Insert a blank before the newline, to avoid # Insert a blank before the newline, to avoid
# ! LaTeX Error: There's no line here to end. # ! LaTeX Error: There's no line here to end.
@ -2614,33 +2617,31 @@ class LaTeXTranslator(SphinxTranslator):
ret = [] # type: List[str] ret = [] # type: List[str]
figure = self.builder.config.numfig_format['figure'].split('%s', 1) figure = self.builder.config.numfig_format['figure'].split('%s', 1)
if len(figure) == 1: if len(figure) == 1:
ret.append('\\def\\fnum@figure{%s}\n' % ret.append('\\def\\fnum@figure{%s}\n' % self.escape(figure[0]).strip())
str(figure[0]).strip().translate(tex_escape_map))
else: else:
definition = escape_abbr(str(figure[0]).translate(tex_escape_map)) definition = escape_abbr(self.escape(figure[0]))
ret.append(self.babel_renewcommand('\\figurename', definition)) ret.append(self.babel_renewcommand('\\figurename', definition))
ret.append('\\makeatletter\n') ret.append('\\makeatletter\n')
ret.append('\\def\\fnum@figure{\\figurename\\thefigure{}%s}\n' % ret.append('\\def\\fnum@figure{\\figurename\\thefigure{}%s}\n' %
str(figure[1]).translate(tex_escape_map)) self.escape(figure[1]))
ret.append('\\makeatother\n') ret.append('\\makeatother\n')
table = self.builder.config.numfig_format['table'].split('%s', 1) table = self.builder.config.numfig_format['table'].split('%s', 1)
if len(table) == 1: if len(table) == 1:
ret.append('\\def\\fnum@table{%s}\n' % ret.append('\\def\\fnum@table{%s}\n' % self.escape(table[0]).strip())
str(table[0]).strip().translate(tex_escape_map))
else: else:
definition = escape_abbr(str(table[0]).translate(tex_escape_map)) definition = escape_abbr(self.escape(table[0]))
ret.append(self.babel_renewcommand('\\tablename', definition)) ret.append(self.babel_renewcommand('\\tablename', definition))
ret.append('\\makeatletter\n') ret.append('\\makeatletter\n')
ret.append('\\def\\fnum@table{\\tablename\\thetable{}%s}\n' % ret.append('\\def\\fnum@table{\\tablename\\thetable{}%s}\n' %
str(table[1]).translate(tex_escape_map)) self.escape(table[1]))
ret.append('\\makeatother\n') ret.append('\\makeatother\n')
codeblock = self.builder.config.numfig_format['code-block'].split('%s', 1) codeblock = self.builder.config.numfig_format['code-block'].split('%s', 1)
if len(codeblock) == 1: if len(codeblock) == 1:
pass # FIXME pass # FIXME
else: else:
definition = str(codeblock[0]).strip().translate(tex_escape_map) definition = self.escape(codeblock[0]).strip()
ret.append(self.babel_renewcommand('\\literalblockname', definition)) ret.append(self.babel_renewcommand('\\literalblockname', definition))
if codeblock[1]: if codeblock[1]:
pass # FIXME pass # FIXME

View File

View File

@ -0,0 +1,7 @@
test-latex-unicode
==================
* script small e:
* double struck italic small i:
* superscript: ⁰, ¹
* subscript: ₀, ₁

View File

@ -105,6 +105,8 @@ def test_add_is_parallel_allowed(app, status, warning):
app.setup_extension('read_serial') app.setup_extension('read_serial')
assert app.is_parallel_allowed('read') is False assert app.is_parallel_allowed('read') is False
assert "the read_serial extension is not safe for parallel reading" in warning.getvalue()
warning.truncate(0) # reset warnings
assert app.is_parallel_allowed('write') is True assert app.is_parallel_allowed('write') is True
assert warning.getvalue() == '' assert warning.getvalue() == ''
app.extensions.pop('read_serial') app.extensions.pop('read_serial')

View File

@ -1437,3 +1437,35 @@ def test_index_on_title(app, status, warning):
'\\label{\\detokenize{contents:test-for-index-in-top-level-title}}' '\\label{\\detokenize{contents:test-for-index-in-top-level-title}}'
'\\index{index@\\spxentry{index}}\n' '\\index{index@\\spxentry{index}}\n'
in result) in result)
@pytest.mark.sphinx('latex', testroot='latex-unicode',
confoverrides={'latex_engine': 'pdflatex'})
def test_texescape_for_non_unicode_supported_engine(app, status, warning):
app.builder.build_all()
result = (app.outdir / 'python.tex').text()
print(result)
assert 'script small e: e' in result
assert 'double struck italic small i: i' in result
assert r'superscript: \(\sp{\text{0}}\), \(\sp{\text{1}}\)' in result
assert r'subscript: \(\sb{\text{0}}\), \(\sb{\text{1}}\)' in result
@pytest.mark.sphinx('latex', testroot='latex-unicode',
confoverrides={'latex_engine': 'xelatex'})
def test_texescape_for_unicode_supported_engine(app, status, warning):
app.builder.build_all()
result = (app.outdir / 'python.tex').text()
print(result)
assert 'script small e: e' in result
assert 'double struck italic small i: i' in result
assert 'superscript: ⁰, ¹' in result
assert 'subscript: ₀, ₁' in result
@pytest.mark.sphinx('latex', testroot='basic',
confoverrides={'latex_elements': {'extrapackages': r'\usepackage{foo}'}})
def test_latex_elements_extrapackages(app, status, warning):
app.builder.build_all()
result = (app.outdir / 'test.tex').text()
assert r'\usepackage{foo}' in result

View File

@ -312,6 +312,24 @@ def test_inline(get_verifier, type, rst, html_expected, latex_expected):
verifier(rst, html_expected, latex_expected) verifier(rst, html_expected, latex_expected)
@pytest.mark.sphinx(confoverrides={'latex_engine': 'xelatex'})
@pytest.mark.parametrize('type,rst,html_expected,latex_expected', [
(
# in verbatim code fragments
'verify',
'::\n\n\\∞${}',
None,
('\\begin{sphinxVerbatim}[commandchars=\\\\\\{\\}]\n'
'\\PYGZbs{}\\PYGZdl{}\\PYGZob{}\\PYGZcb{}\n'
'\\end{sphinxVerbatim}'),
),
])
def test_inline_for_unicode_latex_engine(get_verifier, type, rst,
html_expected, latex_expected):
verifier = get_verifier(type)
verifier(rst, html_expected, latex_expected)
def test_samp_role(parse): def test_samp_role(parse):
# no braces # no braces
text = ':samp:`a{b}c`' text = ':samp:`a{b}c`'