Merge pull request #3666 from mitya57/1.6-release

Remove the custom smartypants code [rebased]
This commit is contained in:
Takeshi KOMIYA 2017-04-25 23:25:31 +09:00 committed by GitHub
commit 961432ed96
17 changed files with 321 additions and 518 deletions

View File

@ -768,6 +768,12 @@ that use Sphinx's HTMLWriter class.
will be used to convert quotes and dashes to typographically correct
entities. Default: ``True``.
.. deprecated:: 1.6
Use the `smart_quotes option`_ in the Docutils configuration file
(``docutils.conf``) instead.
.. _`smart_quotes option`: http://docutils.sourceforge.net/docs/user/config.html#smart-quotes
.. confval:: html_add_permalinks
Sphinx will add "permalinks" for each heading and description environment as

View File

@ -25,6 +25,7 @@ except ImportError:
Image = None
from docutils import nodes
from docutils.utils import smartquotes
from sphinx import addnodes
from sphinx.builders.html import StandaloneHTMLBuilder
@ -32,7 +33,6 @@ from sphinx.util import logging
from sphinx.util import status_iterator
from sphinx.util.osutil import ensuredir, copyfile
from sphinx.util.fileutil import copy_asset_file
from sphinx.util.smartypants import sphinx_smarty_pants as ssp
if False:
# For type annotation
@ -94,6 +94,18 @@ Guide = namedtuple('Guide', ['type', 'title', 'uri'])
NavPoint = namedtuple('NavPoint', ['navpoint', 'playorder', 'text', 'refuri', 'children'])
def sphinx_smarty_pants(t, language='en'):
# type: (unicode, str) -> unicode
t = t.replace('"', '"')
t = smartquotes.educateDashesOldSchool(t)
t = smartquotes.educateQuotes(t, language)
t = t.replace('"', '"')
return t
ssp = sphinx_smarty_pants
# The epub publisher
class EpubBuilder(StandaloneHTMLBuilder):

View File

@ -45,8 +45,7 @@ from sphinx.builders import Builder
from sphinx.application import ENV_PICKLE_FILENAME
from sphinx.highlighting import PygmentsBridge
from sphinx.util.console import bold, darkgreen # type: ignore
from sphinx.writers.html import HTMLWriter, HTMLTranslator, \
SmartyPantsHTMLTranslator
from sphinx.writers.html import HTMLWriter, HTMLTranslator
from sphinx.environment.adapters.asset import ImageAdapter
from sphinx.environment.adapters.toctree import TocTree
from sphinx.environment.adapters.indexentries import IndexEntries
@ -59,7 +58,7 @@ if False:
# Experimental HTML5 Writer
if is_html5_writer_available():
from sphinx.writers.html5 import HTML5Translator, SmartyPantsHTML5Translator
from sphinx.writers.html5 import HTML5Translator
html5_ready = True
else:
html5_ready = False
@ -220,15 +219,9 @@ class StandaloneHTMLBuilder(Builder):
@property
def default_translator_class(self):
if self.config.html_experimental_html5_writer and html5_ready:
if self.config.html_use_smartypants:
return SmartyPantsHTML5Translator
else:
return HTML5Translator
return HTML5Translator
else:
if self.config.html_use_smartypants:
return SmartyPantsHTMLTranslator
else:
return HTMLTranslator
return HTMLTranslator
def get_outdated_docs(self):
# type: () -> Iterator[unicode]
@ -1319,7 +1312,7 @@ def setup(app):
app.add_config_value('html_static_path', [], 'html')
app.add_config_value('html_extra_path', [], 'html')
app.add_config_value('html_last_updated_fmt', None, 'html', string_classes)
app.add_config_value('html_use_smartypants', True, 'html')
app.add_config_value('html_use_smartypants', None, 'html')
app.add_config_value('html_sidebars', {}, 'html')
app.add_config_value('html_additional_pages', {}, 'html')
app.add_config_value('html_domain_indices', True, 'html', [list])

View File

@ -26,6 +26,7 @@ from six.moves import cPickle as pickle
from docutils.io import NullOutput
from docutils.core import Publisher
from docutils.utils import Reporter, get_source_line
from docutils.utils.smartquotes import smartchars
from docutils.parsers.rst import roles
from docutils.parsers.rst.languages import en as english
from docutils.frontend import OptionParser
@ -671,6 +672,16 @@ class BuildEnvironment(object):
self.settings['trim_footnote_reference_space'] = \
self.config.trim_footnote_reference_space
self.settings['gettext_compact'] = self.config.gettext_compact
language = (self.config.language or 'en').replace('_', '-')
self.settings['language_code'] = language
if self.config.html_use_smartypants is not None:
warnings.warn("html_use_smartypants option is deprecated. Use the "
"smart_quotes option in docutils.conf instead.",
RemovedInSphinx17Warning)
if language in smartchars.quotes:
self.settings['smart_quotes'] = self.config.html_use_smartypants
elif language in smartchars.quotes: # We enable smartypants by default
self.settings['smart_quotes'] = True
docutilsconf = path.join(self.srcdir, 'docutils.conf')
# read docutils.conf from source dir, not from current dir

View File

@ -1388,10 +1388,6 @@
\protected\def\sphinxstyleabbreviation {\textsc}
\protected\def\sphinxstyleliteralintitle {\sphinxcode}
% LaTeX writer uses macros to hide double quotes from \sphinxcode's \@noligs
\protected\def\sphinxquotedblleft{``}
\protected\def\sphinxquotedblright{''}
% Tell TeX about pathological hyphenation cases:
\hyphenation{Base-HTTP-Re-quest-Hand-ler}
\endinput

View File

@ -34,6 +34,7 @@ from sphinx.util import logging
from sphinx.util.console import strip_colors, colorize, bold, term_width_line # type: ignore
from sphinx.util.fileutil import copy_asset_file
from sphinx.util.osutil import fs_encoding
from sphinx.util import smartypants # noqa
# import other utilities; partly for backwards compatibility, so don't
# prune unused ones indiscriminately

View File

@ -1,312 +1,280 @@
r"""
This is based on SmartyPants.py by `Chad Miller`_ <smartypantspy@chad.org>,
version 1.5_1.6.
Copyright and License
=====================
SmartyPants_ license::
Copyright (c) 2003 John Gruber
(http://daringfireball.net/)
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in
the documentation and/or other materials provided with the
distribution.
* Neither the name "SmartyPants" nor the names of its contributors
may be used to endorse or promote products derived from this
software without specific prior written permission.
This software is provided by the copyright holders and contributors "as
is" and any express or implied warranties, including, but not limited
to, the implied warranties of merchantability and fitness for a
particular purpose are disclaimed. In no event shall the copyright
owner or contributors be liable for any direct, indirect, incidental,
special, exemplary, or consequential damages (including, but not
limited to, procurement of substitute goods or services; loss of use,
data, or profits; or business interruption) however caused and on any
theory of liability, whether in contract, strict liability, or tort
(including negligence or otherwise) arising in any way out of the use
of this software, even if advised of the possibility of such damage.
smartypants.py license::
smartypants.py is a derivative work of SmartyPants.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in
the documentation and/or other materials provided with the
distribution.
This software is provided by the copyright holders and contributors "as
is" and any express or implied warranties, including, but not limited
to, the implied warranties of merchantability and fitness for a
particular purpose are disclaimed. In no event shall the copyright
owner or contributors be liable for any direct, indirect, incidental,
special, exemplary, or consequential damages (including, but not
limited to, procurement of substitute goods or services; loss of use,
data, or profits; or business interruption) however caused and on any
theory of liability, whether in contract, strict liability, or tort
(including negligence or otherwise) arising in any way out of the use
of this software, even if advised of the possibility of such damage.
.. _Chad Miller: http://web.chad.org/
# -*- coding: utf-8 -*-
"""
sphinx.util.smartypants
~~~~~~~~~~~~~~~~~~~~~~~
This code is copied from docutils docutils/utils/smartquotes.py
version 1.7.1 (from 2017-03-19). It should be removed in the future.
:copyright: © 2010 Günter Milde,
original `SmartyPants`_: © 2003 John Gruber
smartypants.py: © 2004, 2007 Chad Miller
:license: Released under the terms of the `2-Clause BSD license`_, in short:
Copying and distribution of this file, with or without modification,
are permitted in any medium without royalty provided the copyright
notices and this notice are preserved.
This file is offered as-is, without any warranty.
.. _SmartyPants: http://daringfireball.net/projects/smartypants/
.. _2-Clause BSD license: http://www.spdx.org/licenses/BSD-2-Clause
See the LICENSE file and the original docutils code for details.
"""
from __future__ import absolute_import, unicode_literals
import re
from docutils.utils import smartquotes
from sphinx.util.docutils import __version_info__ as docutils_version
if False:
# For type annotation
from typing import Tuple # NOQA
if False: # For type annotation
from typing import Iterable, Iterator, Tuple # NOQA
def sphinx_smarty_pants(t):
# type: (unicode) -> unicode
t = t.replace('&quot;', '"')
t = educate_dashes_oldschool(t)
t = educate_quotes(t) # type: ignore
t = t.replace('"', '&quot;')
return t
# Constants for quote education.
punct_class = r"""[!"#\$\%'()*+,-.\/:;<=>?\@\[\\\]\^_`{|}~]"""
end_of_word_class = r"""[\s.,;:!?)]"""
close_class = r"""[^\ \t\r\n\[\{\(\-]"""
dec_dashes = r"""&#8211;|&#8212;"""
# Special case if the very first character is a quote
# followed by punctuation at a non-word-break. Close the quotes by brute force:
single_quote_start_re = re.compile(r"""^'(?=%s\\B)""" % (punct_class,))
double_quote_start_re = re.compile(r"""^"(?=%s\\B)""" % (punct_class,))
# Special case for double sets of quotes, e.g.:
# <p>He said, "'Quoted' words in a larger quote."</p>
double_quote_sets_re = re.compile(r""""'(?=\w)""")
single_quote_sets_re = re.compile(r"""'"(?=\w)""")
# Special case for decade abbreviations (the '80s):
decade_abbr_re = re.compile(r"""\b'(?=\d{2}s)""")
# Get most opening double quotes:
opening_double_quotes_regex = re.compile(r"""
(
\s | # a whitespace char, or
&#160; | # a non-breaking space entity, or
-- | # dashes, or
&[mn]dash; | # named dash entities
%s | # or decimal entities
&\#x201[34]; # or hex
)
" # the quote
(?=\w) # followed by a word character
""" % (dec_dashes,), re.VERBOSE)
# Double closing quotes:
closing_double_quotes_regex = re.compile(r"""
#(%s)? # character that indicates the quote should be closing
"
(?=%s)
""" % (close_class, end_of_word_class), re.VERBOSE)
closing_double_quotes_regex_2 = re.compile(r"""
(%s) # character that indicates the quote should be closing
"
""" % (close_class,), re.VERBOSE)
# Get most opening single quotes:
opening_single_quotes_regex = re.compile(r"""
(
\s | # a whitespace char, or
&#160; | # a non-breaking space entity, or
-- | # dashes, or
&[mn]dash; | # named dash entities
%s | # or decimal entities
&\#x201[34]; # or hex
)
' # the quote
(?=\w) # followed by a word character
""" % (dec_dashes,), re.VERBOSE)
closing_single_quotes_regex = re.compile(r"""
(%s)
'
(?!\s | s\b | \d)
""" % (close_class,), re.VERBOSE)
closing_single_quotes_regex_2 = re.compile(r"""
(%s)
'
(\s | s\b)
""" % (close_class,), re.VERBOSE)
def educate_quotes(s):
# type: (str) -> str
def educateQuotes(text, language='en'):
# type: (unicode, unicode) -> unicode
"""
Parameter: String.
Returns: The string, with "educated" curly quote HTML entities.
Parameter: - text string (unicode or bytes).
- language (`BCP 47` language tag.)
Returns: The `text`, with "educated" curly quote characters.
Example input: "Isn't this fun?"
Example output: &#8220;Isn&#8217;t this fun?&#8221;
Example output: Isnt this fun?;
"""
smart = smartquotes.smartchars(language)
smart.apostrophe = u''
# oldtext = text
punct_class = r"""[!"#\$\%'()*+,-.\/:;<=>?\@\[\\\]\^_`{|}~]"""
# Special case if the very first character is a quote
# followed by punctuation at a non-word-break. Close the quotes
# by brute force:
s = single_quote_start_re.sub("&#8217;", s)
s = double_quote_start_re.sub("&#8221;", s)
# followed by punctuation at a non-word-break.
# Close the quotes by brute force:
text = re.sub(r"""^'(?=%s\\B)""" % (punct_class,), smart.csquote, text)
text = re.sub(r"""^"(?=%s\\B)""" % (punct_class,), smart.cpquote, text)
# Special case for double sets of quotes, e.g.:
# <p>He said, "'Quoted' words in a larger quote."</p>
s = double_quote_sets_re.sub("&#8220;&#8216;", s)
s = single_quote_sets_re.sub("&#8216;&#8220;", s)
text = re.sub(r""""'(?=\w)""", smart.opquote + smart.osquote, text)
text = re.sub(r"""'"(?=\w)""", smart.osquote + smart.opquote, text)
# Special case for decade abbreviations (the '80s):
s = decade_abbr_re.sub("&#8217;", s)
text = re.sub(r"""\b'(?=\d{2}s)""", smart.csquote, text)
s = opening_single_quotes_regex.sub(r"\1&#8216;", s)
s = closing_single_quotes_regex.sub(r"\1&#8217;", s)
s = closing_single_quotes_regex_2.sub(r"\1&#8217;\2", s)
close_class = r"""[^\ \t\r\n\[\{\(\-]"""
dec_dashes = r"""&#8211;|&#8212;"""
# Get most opening single quotes:
opening_single_quotes_regex = re.compile(r"""
(
\s | # a whitespace char, or
&nbsp; | # a non-breaking space entity, or
-- | # dashes, or
&[mn]dash; | # named dash entities
%s | # or decimal entities
&\#x201[34]; # or hex
)
' # the quote
(?=\w) # followed by a word character
""" % (dec_dashes,), re.VERBOSE | re.UNICODE)
text = opening_single_quotes_regex.sub(r'\1' + smart.osquote, text)
# In many locales, single closing quotes are different from apostrophe:
if smart.csquote != smart.apostrophe:
apostrophe_regex = re.compile(r"(?<=(\w|\d))'(?=\w)", re.UNICODE)
text = apostrophe_regex.sub(smart.apostrophe, text)
closing_single_quotes_regex = re.compile(r"""
(%s)
'
(?!\s | # whitespace
s\b |
\d # digits ('80s)
)
""" % (close_class,), re.VERBOSE | re.UNICODE)
text = closing_single_quotes_regex.sub(r'\1' + smart.csquote, text)
closing_single_quotes_regex = re.compile(r"""
(%s)
'
(\s | s\b)
""" % (close_class,), re.VERBOSE | re.UNICODE)
text = closing_single_quotes_regex.sub(r'\1%s\2' % smart.csquote, text)
# Any remaining single quotes should be opening ones:
s = s.replace("'", "&#8216;")
text = re.sub(r"""'""", smart.osquote, text)
s = opening_double_quotes_regex.sub(r"\1&#8220;", s)
s = closing_double_quotes_regex.sub(r"&#8221;", s)
s = closing_double_quotes_regex_2.sub(r"\1&#8221;", s)
# Get most opening double quotes:
opening_double_quotes_regex = re.compile(r"""
(
\s | # a whitespace char, or
&nbsp; | # a non-breaking space entity, or
-- | # dashes, or
&[mn]dash; | # named dash entities
%s | # or decimal entities
&\#x201[34]; # or hex
)
" # the quote
(?=\w) # followed by a word character
""" % (dec_dashes,), re.VERBOSE)
text = opening_double_quotes_regex.sub(r'\1' + smart.opquote, text)
# Double closing quotes:
closing_double_quotes_regex = re.compile(r"""
#(%s)? # character that indicates the quote should be closing
"
(?=\s)
""" % (close_class,), re.VERBOSE)
text = closing_double_quotes_regex.sub(smart.cpquote, text)
closing_double_quotes_regex = re.compile(r"""
(%s) # character that indicates the quote should be closing
"
""" % (close_class,), re.VERBOSE)
text = closing_double_quotes_regex.sub(r'\1' + smart.cpquote, text)
# Any remaining quotes should be opening ones.
return s.replace('"', "&#8220;")
text = re.sub(r'"', smart.opquote, text)
return text
def educate_quotes_latex(s, dquotes=("``", "''")):
# type: (str, Tuple[str, str]) -> unicode
"""
Parameter: String.
Returns: The string, with double quotes corrected to LaTeX quotes.
Example input: "Isn't this fun?"
Example output: ``Isn't this fun?'';
def educate_tokens(text_tokens, attr='1', language='en'):
# type: (Iterable[Tuple[str, unicode]], unicode, unicode) -> Iterator
"""Return iterator that "educates" the items of `text_tokens`.
"""
# Special case if the very first character is a quote
# followed by punctuation at a non-word-break. Close the quotes
# by brute force:
s = single_quote_start_re.sub("\x04", s)
s = double_quote_start_re.sub("\x02", s)
# Parse attributes:
# 0 : do nothing
# 1 : set all
# 2 : set all, using old school en- and em- dash shortcuts
# 3 : set all, using inverted old school en and em- dash shortcuts
#
# q : quotes
# b : backtick quotes (``double'' only)
# B : backtick quotes (``double'' and `single')
# d : dashes
# D : old school dashes
# i : inverted old school dashes
# e : ellipses
# w : convert &quot; entities to " for Dreamweaver users
# Special case for double sets of quotes, e.g.:
# <p>He said, "'Quoted' words in a larger quote."</p>
s = double_quote_sets_re.sub("\x01\x03", s)
s = single_quote_sets_re.sub("\x03\x01", s)
convert_quot = False # translate &quot; entities into normal quotes?
do_dashes = 0
do_backticks = 0
do_quotes = False
do_ellipses = False
do_stupefy = False
# Special case for decade abbreviations (the '80s):
s = decade_abbr_re.sub("\x04", s)
if attr == "0": # Do nothing.
pass
elif attr == "1": # Do everything, turn all options on.
do_quotes = True
do_backticks = 1
do_dashes = 1
do_ellipses = True
elif attr == "2":
# Do everything, turn all options on, use old school dash shorthand.
do_quotes = True
do_backticks = 1
do_dashes = 2
do_ellipses = True
elif attr == "3":
# Do everything, use inverted old school dash shorthand.
do_quotes = True
do_backticks = 1
do_dashes = 3
do_ellipses = True
elif attr == "-1": # Special "stupefy" mode.
do_stupefy = True
else:
if "q" in attr:
do_quotes = True
if "b" in attr:
do_backticks = 1
if "B" in attr:
do_backticks = 2
if "d" in attr:
do_dashes = 1
if "D" in attr:
do_dashes = 2
if "i" in attr:
do_dashes = 3
if "e" in attr:
do_ellipses = True
if "w" in attr:
convert_quot = True
s = opening_single_quotes_regex.sub("\\1\x03", s)
s = closing_single_quotes_regex.sub("\\1\x04", s)
s = closing_single_quotes_regex_2.sub("\\1\x04\\2", s)
prev_token_last_char = " "
# Last character of the previous text token. Used as
# context to curl leading quote characters correctly.
# Any remaining single quotes should be opening ones:
s = s.replace("'", "\x03")
for (ttype, text) in text_tokens:
s = opening_double_quotes_regex.sub("\\1\x01", s)
s = closing_double_quotes_regex.sub("\x02", s)
s = closing_double_quotes_regex_2.sub("\\1\x02", s)
# skip HTML and/or XML tags as well as emtpy text tokens
# without updating the last character
if ttype == 'tag' or not text:
yield text
continue
# Any remaining quotes should be opening ones.
s = s.replace('"', "\x01")
# skip literal text (math, literal, raw, ...)
if ttype == 'literal':
prev_token_last_char = text[-1:]
yield text
continue
# Finally, replace all helpers with quotes.
return s.replace("\x01", dquotes[0]).replace("\x02", dquotes[1]).\
replace("\x03", "`").replace("\x04", "'")
last_char = text[-1:] # Remember last char before processing.
text = smartquotes.processEscapes(text)
if convert_quot:
text = re.sub('&quot;', '"', text)
if do_dashes == 1:
text = smartquotes.educateDashes(text)
elif do_dashes == 2:
text = smartquotes.educateDashesOldSchool(text)
elif do_dashes == 3:
text = smartquotes.educateDashesOldSchoolInverted(text)
if do_ellipses:
text = smartquotes.educateEllipses(text)
# Note: backticks need to be processed before quotes.
if do_backticks:
text = smartquotes.educateBackticks(text, language)
if do_backticks == 2:
text = smartquotes.educateSingleBackticks(text, language)
if do_quotes:
# Replace plain quotes to prevent converstion to
# 2-character sequence in French.
context = prev_token_last_char.replace('"', ';').replace("'", ';')
text = educateQuotes(context + text, language)[1:]
if do_stupefy:
text = smartquotes.stupefyEntities(text, language)
# Remember last char as context for the next token
prev_token_last_char = last_char
text = smartquotes.processEscapes(text, restore=True)
yield text
def educate_backticks(s):
# type: (unicode) -> unicode
"""
Parameter: String.
Returns: The string, with ``backticks'' -style double quotes
translated into HTML curly quote entities.
Example input: ``Isn't this fun?''
Example output: &#8220;Isn't this fun?&#8221;
"""
return s.replace("``", "&#8220;").replace("''", "&#8221;")
if docutils_version < (0, 13, 2):
# Monkey patch the old docutils versions to fix the issue mentioned
# at https://sourceforge.net/p/docutils/bugs/313/
smartquotes.educateQuotes = educateQuotes
# And the one mentioned at https://sourceforge.net/p/docutils/bugs/317/
smartquotes.educate_tokens = educate_tokens
def educate_single_backticks(s):
# type: (unicode) -> unicode
"""
Parameter: String.
Returns: The string, with `backticks' -style single quotes
translated into HTML curly quote entities.
Example input: `Isn't this fun?'
Example output: &#8216;Isn&#8217;t this fun?&#8217;
"""
return s.replace('`', "&#8216;").replace("'", "&#8217;")
def educate_dashes_oldschool(s):
# type: (unicode) -> unicode
"""
Parameter: String.
Returns: The string, with each instance of "--" translated to
an en-dash HTML entity, and each "---" translated to
an em-dash HTML entity.
"""
return s.replace('---', "&#8212;").replace('--', "&#8211;")
def educate_dashes_oldschool_inverted(s):
# type: (unicode) -> unicode
"""
Parameter: String.
Returns: The string, with each instance of "--" translated to
an em-dash HTML entity, and each "---" translated to
an en-dash HTML entity. Two reasons why: First, unlike the
en- and em-dash syntax supported by
educate_dashes_oldschool(), it's compatible with existing
entries written before SmartyPants 1.1, back when "--" was
only used for em-dashes. Second, em-dashes are more
common than en-dashes, and so it sort of makes sense that
the shortcut should be shorter to type. (Thanks to Aaron
Swartz for the idea.)
"""
return s.replace('---', "&#8211;").replace('--', "&#8212;")
def educate_ellipses(s):
# type: (unicode) -> unicode
"""
Parameter: String.
Returns: The string, with each instance of "..." translated to
an ellipsis HTML entity.
Example input: Huh...?
Example output: Huh&#8230;?
"""
return s.replace('...', "&#8230;").replace('. . .', "&#8230;")
# Fix the issue with French quotes mentioned at
# https://sourceforge.net/p/docutils/mailman/message/35760696/
quotes = smartquotes.smartchars.quotes
quotes['fr'] = (u'«\u00a0', u'\u00a0»', u'', u'')
quotes['fr-ch'] = u'«»‹›'

View File

@ -22,7 +22,6 @@ from sphinx import addnodes
from sphinx.locale import admonitionlabels, _
from sphinx.util import logging
from sphinx.util.images import get_image_size
from sphinx.util.smartypants import sphinx_smarty_pants
if False:
# For type annotation
@ -74,7 +73,6 @@ class HTMLTranslator(BaseTranslator):
# type: (StandaloneHTMLBuilder, Any, Any) -> None
BaseTranslator.__init__(self, *args, **kwds)
self.highlighter = builder.highlighter
self.no_smarty = 0
self.builder = builder
self.highlightlang = self.highlightlang_base = \
builder.config.highlight_language
@ -685,10 +683,6 @@ class HTMLTranslator(BaseTranslator):
BaseTranslator.visit_option_group(self, node)
self.context[-2] = self.context[-2].replace('&nbsp;', '&#160;')
def bulk_text_processor(self, text):
# type: (unicode) -> unicode
return text
# overwritten
def visit_Text(self, node):
# type: (nodes.Node) -> None
@ -710,8 +704,6 @@ class HTMLTranslator(BaseTranslator):
else:
if self.in_mailto and self.settings.cloak_email_addresses:
encoded = self.cloak_email(encoded)
else:
encoded = self.bulk_text_processor(encoded)
self.body.append(encoded)
def visit_note(self, node):
@ -786,7 +778,6 @@ class HTMLTranslator(BaseTranslator):
# type: (nodes.Node) -> None
self.depart_admonition(node)
# these are only handled specially in the SmartyPantsHTMLTranslator
def visit_literal_emphasis(self, node):
# type: (nodes.Node) -> None
return self.visit_emphasis(node)
@ -875,94 +866,3 @@ class HTMLTranslator(BaseTranslator):
def unknown_visit(self, node):
# type: (nodes.Node) -> None
raise NotImplementedError('Unknown node: ' + node.__class__.__name__)
class SmartyPantsHTMLTranslator(HTMLTranslator):
"""
Handle ordinary text via smartypants, converting quotes and dashes
to the correct entities.
"""
def __init__(self, *args, **kwds):
# type: (Any, Any) -> None
self.no_smarty = 0
HTMLTranslator.__init__(self, *args, **kwds)
def visit_literal(self, node):
# type: (nodes.Node) -> None
self.no_smarty += 1
try:
# this raises SkipNode
HTMLTranslator.visit_literal(self, node)
finally:
self.no_smarty -= 1
def visit_literal_block(self, node):
# type: (nodes.Node) -> None
self.no_smarty += 1
try:
HTMLTranslator.visit_literal_block(self, node)
except nodes.SkipNode:
# HTMLTranslator raises SkipNode for simple literal blocks,
# but not for parsed literal blocks
self.no_smarty -= 1
raise
def depart_literal_block(self, node):
# type: (nodes.Node) -> None
HTMLTranslator.depart_literal_block(self, node)
self.no_smarty -= 1
def visit_literal_emphasis(self, node):
# type: (nodes.Node) -> None
self.no_smarty += 1
self.visit_emphasis(node)
def depart_literal_emphasis(self, node):
# type: (nodes.Node) -> None
self.depart_emphasis(node)
self.no_smarty -= 1
def visit_literal_strong(self, node):
# type: (nodes.Node) -> None
self.no_smarty += 1
self.visit_strong(node)
def depart_literal_strong(self, node):
# type: (nodes.Node) -> None
self.depart_strong(node)
self.no_smarty -= 1
def visit_desc_signature(self, node):
# type: (nodes.Node) -> None
self.no_smarty += 1
HTMLTranslator.visit_desc_signature(self, node)
def depart_desc_signature(self, node):
# type: (nodes.Node) -> None
self.no_smarty -= 1
HTMLTranslator.depart_desc_signature(self, node)
def visit_productionlist(self, node):
# type: (nodes.Node) -> None
self.no_smarty += 1
try:
HTMLTranslator.visit_productionlist(self, node)
finally:
self.no_smarty -= 1
def visit_option(self, node):
# type: (nodes.Node) -> None
self.no_smarty += 1
HTMLTranslator.visit_option(self, node)
def depart_option(self, node):
# type: (nodes.Node) -> None
self.no_smarty -= 1
HTMLTranslator.depart_option(self, node)
def bulk_text_processor(self, text):
# type: (unicode) -> unicode
if self.no_smarty <= 0:
return sphinx_smarty_pants(text)
return text

View File

@ -21,7 +21,6 @@ from sphinx import addnodes
from sphinx.locale import admonitionlabels, _
from sphinx.util import logging
from sphinx.util.images import get_image_size
from sphinx.util.smartypants import sphinx_smarty_pants
if False:
# For type annotation
@ -44,7 +43,6 @@ class HTML5Translator(BaseTranslator):
# type: (StandaloneHTMLBuilder, Any, Any) -> None
BaseTranslator.__init__(self, *args, **kwds)
self.highlighter = builder.highlighter
self.no_smarty = 0
self.builder = builder
self.highlightlang = self.highlightlang_base = \
builder.config.highlight_language
@ -628,10 +626,6 @@ class HTML5Translator(BaseTranslator):
# type: (nodes.Node) -> None
self.body.append('</td>')
def bulk_text_processor(self, text):
# type: (unicode) -> unicode
return text
# overwritten
def visit_Text(self, node):
# type: (nodes.Node) -> None
@ -653,8 +647,6 @@ class HTML5Translator(BaseTranslator):
else:
if self.in_mailto and self.settings.cloak_email_addresses:
encoded = self.cloak_email(encoded)
else:
encoded = self.bulk_text_processor(encoded)
self.body.append(encoded)
def visit_note(self, node):
@ -729,7 +721,6 @@ class HTML5Translator(BaseTranslator):
# type: (nodes.Node) -> None
self.depart_admonition(node)
# these are only handled specially in the SmartyPantsHTML5Translator
def visit_literal_emphasis(self, node):
# type: (nodes.Node) -> None
return self.visit_emphasis(node)
@ -830,94 +821,3 @@ class HTML5Translator(BaseTranslator):
def unknown_visit(self, node):
# type: (nodes.Node) -> None
raise NotImplementedError('Unknown node: ' + node.__class__.__name__)
class SmartyPantsHTML5Translator(HTML5Translator):
"""
Handle ordinary text via smartypants, converting quotes and dashes
to the correct entities.
"""
def __init__(self, *args, **kwds):
# type: (Any, Any) -> None
self.no_smarty = 0
HTML5Translator.__init__(self, *args, **kwds)
def visit_literal(self, node):
# type: (nodes.Node) -> None
self.no_smarty += 1
try:
# this raises SkipNode
HTML5Translator.visit_literal(self, node)
finally:
self.no_smarty -= 1
def visit_literal_block(self, node):
# type: (nodes.Node) -> None
self.no_smarty += 1
try:
HTML5Translator.visit_literal_block(self, node)
except nodes.SkipNode:
# HTML5Translator raises SkipNode for simple literal blocks,
# but not for parsed literal blocks
self.no_smarty -= 1
raise
def depart_literal_block(self, node):
# type: (nodes.Node) -> None
HTML5Translator.depart_literal_block(self, node)
self.no_smarty -= 1
def visit_literal_emphasis(self, node):
# type: (nodes.Node) -> None
self.no_smarty += 1
self.visit_emphasis(node)
def depart_literal_emphasis(self, node):
# type: (nodes.Node) -> None
self.depart_emphasis(node)
self.no_smarty -= 1
def visit_literal_strong(self, node):
# type: (nodes.Node) -> None
self.no_smarty += 1
self.visit_strong(node)
def depart_literal_strong(self, node):
# type: (nodes.Node) -> None
self.depart_strong(node)
self.no_smarty -= 1
def visit_desc_signature(self, node):
# type: (nodes.Node) -> None
self.no_smarty += 1
HTML5Translator.visit_desc_signature(self, node)
def depart_desc_signature(self, node):
# type: (nodes.Node) -> None
self.no_smarty -= 1
HTML5Translator.depart_desc_signature(self, node)
def visit_productionlist(self, node):
# type: (nodes.Node) -> None
self.no_smarty += 1
try:
HTML5Translator.visit_productionlist(self, node)
finally:
self.no_smarty -= 1
def visit_option(self, node):
# type: (nodes.Node) -> None
self.no_smarty += 1
HTML5Translator.visit_option(self, node)
def depart_option(self, node):
# type: (nodes.Node) -> None
self.no_smarty -= 1
HTML5Translator.depart_option(self, node)
def bulk_text_processor(self, text):
# type: (unicode) -> unicode
if self.no_smarty <= 0:
return sphinx_smarty_pants(text)
return text

View File

@ -30,7 +30,6 @@ from sphinx.util.i18n import format_date
from sphinx.util.nodes import clean_astext, traverse_parent
from sphinx.util.template import LaTeXRenderer
from sphinx.util.texescape import tex_escape_map, tex_replace_map
from sphinx.util.smartypants import educate_quotes_latex
if False:
# For type annotation
@ -2533,10 +2532,6 @@ class LaTeXTranslator(nodes.NodeVisitor):
def visit_Text(self, node):
# type: (nodes.Node) -> None
text = self.encode(node.astext())
if not self.no_contractions and not self.in_parsed_literal:
text = educate_quotes_latex(text, # type: ignore
dquotes=("\\sphinxquotedblleft{}",
"\\sphinxquotedblright{}"))
self.body.append(text)
def depart_Text(self, node):

View File

@ -426,6 +426,26 @@ More domains:
.. default-role::
Smart quotes
------------
* Smart "quotes" in English 'text'.
* Smart --- long and -- short dashes.
* Ellipsis...
* No smartypants in literal blocks: ``foo--"bar"...``.
.. only:: html
.. LaTeX does not like Cyrillic letters in this test, so it is HTML only.
.. rst-class:: language-ru
Этот "абзац" должен использовать 'русские' кавычки.
.. rst-class:: language-fr
Il dit : "C'est 'super' !"
.. rubric:: Footnotes
.. [#] Like footnotes.

View File

@ -28,15 +28,6 @@ def test_html_translator(app, status, warning):
# no set_translator()
translator_class = app.builder.get_translator_class()
assert translator_class
assert translator_class.__name__ == 'SmartyPantsHTMLTranslator'
@pytest.mark.sphinx('html', confoverrides={
'html_use_smartypants': False})
def test_html_with_smartypants(app, status, warning):
# no set_translator(), html_use_smartypants=False
translator_class = app.builder.get_translator_class()
assert translator_class
assert translator_class.__name__ == 'HTMLTranslator'

View File

@ -162,7 +162,7 @@ def test_nested_toc(app):
app.build()
# toc.ncx
toc = EPUBElementTree.fromstring((app.outdir / 'toc.ncx').text())
toc = EPUBElementTree.fromstring((app.outdir / 'toc.ncx').bytes())
assert toc.find("./ncx:docTitle/ncx:text").text == 'Python documentation'
# toc.ncx / navPoint
@ -175,7 +175,7 @@ def test_nested_toc(app):
navpoints = toc.findall("./ncx:navMap/ncx:navPoint")
assert len(navpoints) == 4
assert navinfo(navpoints[0]) == ('navPoint1', '1', 'index.xhtml',
"Welcome to Sphinx Tests's documentation!")
u"Welcome to Sphinx Testss documentation!")
assert navpoints[0].findall("./ncx:navPoint") == []
# toc.ncx / nested navPoints
@ -192,11 +192,11 @@ def test_nested_toc(app):
anchor = elem.find("./xhtml:a")
return (anchor.get('href'), anchor.text)
nav = EPUBElementTree.fromstring((app.outdir / 'nav.xhtml').text())
nav = EPUBElementTree.fromstring((app.outdir / 'nav.xhtml').bytes())
toc = nav.findall("./xhtml:body/xhtml:nav/xhtml:ol/xhtml:li")
assert len(toc) == 4
assert navinfo(toc[0]) == ('index.xhtml',
"Welcome to Sphinx Tests's documentation!")
u"Welcome to Sphinx Testss documentation!")
assert toc[0].findall("./xhtml:ol") == []
# nav.xhtml / nested toc

View File

@ -297,6 +297,13 @@ def test_static_output(app):
# tests for ``any`` role
(".//a[@href='#with']/span", 'headings'),
(".//a[@href='objects.html#func_without_body']/code/span", 'objects'),
# tests for smartypants
(".//li", u'Smart “quotes” in English text.'),
(".//li", u'Smart — long and short dashes.'),
(".//li", u'Ellipsis…'),
(".//li//code//span[@class='pre']", 'foo--"bar"...'),
(".//p", u'Этот «абзац» должен использовать „русские“ кавычки.'),
(".//p", u'Il dit : « Cest “super” ! »'),
],
'objects.html': [
(".//dt[@id='mod.Cls.meth1']", ''),

View File

@ -36,7 +36,7 @@ def test_process_doc(app):
list_item)])
assert_node(toctree[0][0],
[compact_paragraph, reference, "Welcome to Sphinx Tests's documentation!"])
[compact_paragraph, reference, u"Welcome to Sphinx Testss documentation!"])
assert_node(toctree[0][0][0], reference, anchorname='')
assert_node(toctree[0][1][0], addnodes.toctree,
caption="Table of Contents", glob=False, hidden=False,
@ -150,7 +150,7 @@ def test_get_toc_for(app):
addnodes.toctree)])],
[list_item, compact_paragraph])]) # [2][0]
assert_node(toctree[0][0],
[compact_paragraph, reference, "Welcome to Sphinx Tests's documentation!"])
[compact_paragraph, reference, u"Welcome to Sphinx Testss documentation!"])
assert_node(toctree[0][1][2],
([compact_paragraph, reference, "subsection"],
[bullet_list, list_item, compact_paragraph, reference, "subsubsection"]))
@ -177,7 +177,7 @@ def test_get_toc_for_only(app):
addnodes.toctree)])],
[list_item, compact_paragraph])]) # [2][0]
assert_node(toctree[0][0],
[compact_paragraph, reference, "Welcome to Sphinx Tests's documentation!"])
[compact_paragraph, reference, u"Welcome to Sphinx Testss documentation!"])
assert_node(toctree[0][1][1],
([compact_paragraph, reference, "Section for HTML"],
[bullet_list, addnodes.toctree]))

View File

@ -14,11 +14,12 @@ import pickle
from docutils import frontend, utils, nodes
from docutils.parsers.rst import Parser as RstParser
from docutils.transforms.universal import SmartQuotes
from sphinx import addnodes
from sphinx.util import texescape
from sphinx.util.docutils import sphinx_domains
from sphinx.writers.html import HTMLWriter, SmartyPantsHTMLTranslator
from sphinx.writers.html import HTMLWriter, HTMLTranslator
from sphinx.writers.latex import LaTeXWriter, LaTeXTranslator
import pytest
@ -31,6 +32,7 @@ def settings(app):
optparser = frontend.OptionParser(
components=(RstParser, HTMLWriter, LaTeXWriter))
settings = optparser.get_default_values()
settings.smart_quotes = True
settings.env = app.builder.env
settings.env.temp_data['docname'] = 'dummy'
domain_context = sphinx_domains(settings.env)
@ -46,6 +48,7 @@ def parse(settings):
document['file'] = 'dummy'
parser = RstParser()
parser.parse(rst, document)
SmartQuotes(document, startnode=None).apply()
for msg in document.traverse(nodes.system_message):
if msg['level'] == 1:
msg.replace_self([])
@ -62,7 +65,7 @@ class ForgivingTranslator:
pass
class ForgivingHTMLTranslator(SmartyPantsHTMLTranslator, ForgivingTranslator):
class ForgivingHTMLTranslator(HTMLTranslator, ForgivingTranslator):
pass
@ -178,8 +181,8 @@ def get_verifier(verify, verify_re):
# verify smarty-pants quotes
'verify',
'"John"',
'<p>&#8220;John&#8221;</p>',
r'\sphinxquotedblleft{}John\sphinxquotedblright{}',
u'<p>“John”</p>',
u"“John”",
),
(
# ... but not in literal text

View File

@ -34,14 +34,14 @@ def test_docinfo(app, status, warning):
'field name': u'This is a generic bibliographic field.',
'field name 2': (u'Generic bibliographic fields may contain multiple '
u'body elements.\n\nLike this.'),
'status': u'This is a "work in progress"',
'status': u'This is a “work in progress”',
'version': u'1',
'copyright': (u'This document has been placed in the public domain. '
u'You\nmay do with it as you wish. You may copy, modify,'
u'\nredistribute, reattribute, sell, buy, rent, lease,\n'
u'destroy, or improve it, quote it at length, excerpt,\n'
u'incorporate, collate, fold, staple, or mutilate it, or '
u'do\nanything else to it that your or anyone else\'s '
u'do\nanything else to it that your or anyone elses '
u'heart\ndesires.'),
'contact': u'goodger@python.org',
'date': u'2006-05-21',