Remove the custom smartypants code

Instead rely on docutils’ ‘smart_quotes’ option which is available
since docutils 0.10.

This adds support for internationalization: our code supported only
English quotes, while docutils code supports 27 different languages.

Closes #498, #580, #3345, #3472.
This commit is contained in:
Dmitry Shachnev 2017-03-14 23:43:04 +03:00
parent 710ddb3880
commit edc7f30b9c
12 changed files with 38 additions and 539 deletions

View File

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

View File

@ -45,8 +45,7 @@ from sphinx.builders import Builder
from sphinx.application import ENV_PICKLE_FILENAME from sphinx.application import ENV_PICKLE_FILENAME
from sphinx.highlighting import PygmentsBridge from sphinx.highlighting import PygmentsBridge
from sphinx.util.console import bold, darkgreen # type: ignore from sphinx.util.console import bold, darkgreen # type: ignore
from sphinx.writers.html import HTMLWriter, HTMLTranslator, \ from sphinx.writers.html import HTMLWriter, HTMLTranslator
SmartyPantsHTMLTranslator
from sphinx.environment.adapters.asset import ImageAdapter from sphinx.environment.adapters.asset import ImageAdapter
from sphinx.environment.adapters.toctree import TocTree from sphinx.environment.adapters.toctree import TocTree
from sphinx.environment.adapters.indexentries import IndexEntries from sphinx.environment.adapters.indexentries import IndexEntries
@ -59,7 +58,7 @@ if False:
# Experimental HTML5 Writer # Experimental HTML5 Writer
if is_html5_writer_available(): if is_html5_writer_available():
from sphinx.writers.html5 import HTML5Translator, SmartyPantsHTML5Translator from sphinx.writers.html5 import HTML5Translator
html5_ready = True html5_ready = True
else: else:
html5_ready = False html5_ready = False
@ -220,13 +219,7 @@ class StandaloneHTMLBuilder(Builder):
@property @property
def default_translator_class(self): def default_translator_class(self):
if self.config.html_experimental_html5_writer and html5_ready: 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: else:
return HTMLTranslator return HTMLTranslator

View File

@ -26,6 +26,7 @@ from six.moves import cPickle as pickle
from docutils.io import NullOutput from docutils.io import NullOutput
from docutils.core import Publisher from docutils.core import Publisher
from docutils.utils import Reporter, get_source_line from docutils.utils import Reporter, get_source_line
from docutils.utils.smartquotes import smartchars
from docutils.parsers.rst import roles from docutils.parsers.rst import roles
from docutils.parsers.rst.languages import en as english from docutils.parsers.rst.languages import en as english
from docutils.frontend import OptionParser from docutils.frontend import OptionParser
@ -671,6 +672,10 @@ class BuildEnvironment(object):
self.settings['trim_footnote_reference_space'] = \ self.settings['trim_footnote_reference_space'] = \
self.config.trim_footnote_reference_space self.config.trim_footnote_reference_space
self.settings['gettext_compact'] = self.config.gettext_compact self.settings['gettext_compact'] = self.config.gettext_compact
language = (self.config.language or 'en').replace('_', '-')
self.settings['language_code'] = language
if language in smartchars.quotes:
self.settings['smart_quotes'] = self.config.html_use_smartypants
docutilsconf = path.join(self.srcdir, 'docutils.conf') docutilsconf = path.join(self.srcdir, 'docutils.conf')
# read docutils.conf from source dir, not from current dir # read docutils.conf from source dir, not from current dir

View File

@ -1,312 +0,0 @@
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/
"""
import re
if False:
# For type annotation
from typing import 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
"""
Parameter: String.
Returns: The string, with "educated" curly quote HTML entities.
Example input: "Isn't this fun?"
Example output: &#8220;Isn&#8217;t this fun?&#8221;
"""
# 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)
# 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)
# Special case for decade abbreviations (the '80s):
s = decade_abbr_re.sub("&#8217;", s)
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)
# Any remaining single quotes should be opening ones:
s = s.replace("'", "&#8216;")
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)
# Any remaining quotes should be opening ones.
return s.replace('"', "&#8220;")
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?'';
"""
# 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)
# 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)
# Special case for decade abbreviations (the '80s):
s = decade_abbr_re.sub("\x04", s)
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)
# Any remaining single quotes should be opening ones:
s = s.replace("'", "\x03")
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)
# Any remaining quotes should be opening ones.
s = s.replace('"', "\x01")
# Finally, replace all helpers with quotes.
return s.replace("\x01", dquotes[0]).replace("\x02", dquotes[1]).\
replace("\x03", "`").replace("\x04", "'")
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;")
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;")

View File

@ -22,7 +22,6 @@ from sphinx import addnodes
from sphinx.locale import admonitionlabels, _ from sphinx.locale import admonitionlabels, _
from sphinx.util import logging from sphinx.util import logging
from sphinx.util.images import get_image_size from sphinx.util.images import get_image_size
from sphinx.util.smartypants import sphinx_smarty_pants
if False: if False:
# For type annotation # For type annotation
@ -74,7 +73,6 @@ class HTMLTranslator(BaseTranslator):
# type: (StandaloneHTMLBuilder, Any, Any) -> None # type: (StandaloneHTMLBuilder, Any, Any) -> None
BaseTranslator.__init__(self, *args, **kwds) BaseTranslator.__init__(self, *args, **kwds)
self.highlighter = builder.highlighter self.highlighter = builder.highlighter
self.no_smarty = 0
self.builder = builder self.builder = builder
self.highlightlang = self.highlightlang_base = \ self.highlightlang = self.highlightlang_base = \
builder.config.highlight_language builder.config.highlight_language
@ -786,7 +784,6 @@ class HTMLTranslator(BaseTranslator):
# type: (nodes.Node) -> None # type: (nodes.Node) -> None
self.depart_admonition(node) self.depart_admonition(node)
# these are only handled specially in the SmartyPantsHTMLTranslator
def visit_literal_emphasis(self, node): def visit_literal_emphasis(self, node):
# type: (nodes.Node) -> None # type: (nodes.Node) -> None
return self.visit_emphasis(node) return self.visit_emphasis(node)
@ -875,94 +872,3 @@ class HTMLTranslator(BaseTranslator):
def unknown_visit(self, node): def unknown_visit(self, node):
# type: (nodes.Node) -> None # type: (nodes.Node) -> None
raise NotImplementedError('Unknown node: ' + node.__class__.__name__) 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.locale import admonitionlabels, _
from sphinx.util import logging from sphinx.util import logging
from sphinx.util.images import get_image_size from sphinx.util.images import get_image_size
from sphinx.util.smartypants import sphinx_smarty_pants
if False: if False:
# For type annotation # For type annotation
@ -44,7 +43,6 @@ class HTML5Translator(BaseTranslator):
# type: (StandaloneHTMLBuilder, Any, Any) -> None # type: (StandaloneHTMLBuilder, Any, Any) -> None
BaseTranslator.__init__(self, *args, **kwds) BaseTranslator.__init__(self, *args, **kwds)
self.highlighter = builder.highlighter self.highlighter = builder.highlighter
self.no_smarty = 0
self.builder = builder self.builder = builder
self.highlightlang = self.highlightlang_base = \ self.highlightlang = self.highlightlang_base = \
builder.config.highlight_language builder.config.highlight_language
@ -729,7 +727,6 @@ class HTML5Translator(BaseTranslator):
# type: (nodes.Node) -> None # type: (nodes.Node) -> None
self.depart_admonition(node) self.depart_admonition(node)
# these are only handled specially in the SmartyPantsHTML5Translator
def visit_literal_emphasis(self, node): def visit_literal_emphasis(self, node):
# type: (nodes.Node) -> None # type: (nodes.Node) -> None
return self.visit_emphasis(node) return self.visit_emphasis(node)
@ -830,94 +827,3 @@ class HTML5Translator(BaseTranslator):
def unknown_visit(self, node): def unknown_visit(self, node):
# type: (nodes.Node) -> None # type: (nodes.Node) -> None
raise NotImplementedError('Unknown node: ' + node.__class__.__name__) 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.nodes import clean_astext, traverse_parent
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 tex_escape_map, tex_replace_map
from sphinx.util.smartypants import educate_quotes_latex
if False: if False:
# For type annotation # For type annotation
@ -2533,10 +2532,6 @@ class LaTeXTranslator(nodes.NodeVisitor):
def visit_Text(self, node): def visit_Text(self, node):
# type: (nodes.Node) -> None # type: (nodes.Node) -> None
text = self.encode(node.astext()) 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) self.body.append(text)
def depart_Text(self, node): def depart_Text(self, node):

View File

@ -28,15 +28,6 @@ def test_html_translator(app, status, warning):
# no set_translator() # no set_translator()
translator_class = app.builder.get_translator_class() translator_class = app.builder.get_translator_class()
assert 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' assert translator_class.__name__ == 'HTMLTranslator'

View File

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

View File

@ -36,7 +36,7 @@ def test_process_doc(app):
list_item)]) list_item)])
assert_node(toctree[0][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][0][0], reference, anchorname='') assert_node(toctree[0][0][0], reference, anchorname='')
assert_node(toctree[0][1][0], addnodes.toctree, assert_node(toctree[0][1][0], addnodes.toctree,
caption="Table of Contents", glob=False, hidden=False, caption="Table of Contents", glob=False, hidden=False,
@ -150,7 +150,7 @@ def test_get_toc_for(app):
addnodes.toctree)])], addnodes.toctree)])],
[list_item, compact_paragraph])]) # [2][0] [list_item, compact_paragraph])]) # [2][0]
assert_node(toctree[0][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], assert_node(toctree[0][1][2],
([compact_paragraph, reference, "subsection"], ([compact_paragraph, reference, "subsection"],
[bullet_list, list_item, compact_paragraph, reference, "subsubsection"])) [bullet_list, list_item, compact_paragraph, reference, "subsubsection"]))
@ -177,7 +177,7 @@ def test_get_toc_for_only(app):
addnodes.toctree)])], addnodes.toctree)])],
[list_item, compact_paragraph])]) # [2][0] [list_item, compact_paragraph])]) # [2][0]
assert_node(toctree[0][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], assert_node(toctree[0][1][1],
([compact_paragraph, reference, "Section for HTML"], ([compact_paragraph, reference, "Section for HTML"],
[bullet_list, addnodes.toctree])) [bullet_list, addnodes.toctree]))

View File

@ -14,11 +14,12 @@ import pickle
from docutils import frontend, utils, nodes from docutils import frontend, utils, nodes
from docutils.parsers.rst import Parser as RstParser from docutils.parsers.rst import Parser as RstParser
from docutils.transforms.universal import SmartQuotes
from sphinx import addnodes from sphinx import addnodes
from sphinx.util import texescape from sphinx.util import texescape
from sphinx.util.docutils import sphinx_domains 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 from sphinx.writers.latex import LaTeXWriter, LaTeXTranslator
import pytest import pytest
@ -31,6 +32,7 @@ def settings(app):
optparser = frontend.OptionParser( optparser = frontend.OptionParser(
components=(RstParser, HTMLWriter, LaTeXWriter)) components=(RstParser, HTMLWriter, LaTeXWriter))
settings = optparser.get_default_values() settings = optparser.get_default_values()
settings.smart_quotes = True
settings.env = app.builder.env settings.env = app.builder.env
settings.env.temp_data['docname'] = 'dummy' settings.env.temp_data['docname'] = 'dummy'
domain_context = sphinx_domains(settings.env) domain_context = sphinx_domains(settings.env)
@ -46,6 +48,7 @@ def parse(settings):
document['file'] = 'dummy' document['file'] = 'dummy'
parser = RstParser() parser = RstParser()
parser.parse(rst, document) parser.parse(rst, document)
SmartQuotes(document, startnode=None).apply()
for msg in document.traverse(nodes.system_message): for msg in document.traverse(nodes.system_message):
if msg['level'] == 1: if msg['level'] == 1:
msg.replace_self([]) msg.replace_self([])
@ -62,7 +65,7 @@ class ForgivingTranslator:
pass pass
class ForgivingHTMLTranslator(SmartyPantsHTMLTranslator, ForgivingTranslator): class ForgivingHTMLTranslator(HTMLTranslator, ForgivingTranslator):
pass pass
@ -178,8 +181,8 @@ def get_verifier(verify, verify_re):
# verify smarty-pants quotes # verify smarty-pants quotes
'verify', 'verify',
'"John"', '"John"',
'<p>&#8220;John&#8221;</p>', u'<p>“John”</p>',
r'\sphinxquotedblleft{}John\sphinxquotedblright{}', u"“John”",
), ),
( (
# ... but not in literal text # ... 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': u'This is a generic bibliographic field.',
'field name 2': (u'Generic bibliographic fields may contain multiple ' 'field name 2': (u'Generic bibliographic fields may contain multiple '
u'body elements.\n\nLike this.'), 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', 'version': u'1',
'copyright': (u'This document has been placed in the public domain. ' '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'You\nmay do with it as you wish. You may copy, modify,'
u'\nredistribute, reattribute, sell, buy, rent, lease,\n' u'\nredistribute, reattribute, sell, buy, rent, lease,\n'
u'destroy, or improve it, quote it at length, excerpt,\n' u'destroy, or improve it, quote it at length, excerpt,\n'
u'incorporate, collate, fold, staple, or mutilate it, or ' 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.'), u'heart\ndesires.'),
'contact': u'goodger@python.org', 'contact': u'goodger@python.org',
'date': u'2006-05-21', 'date': u'2006-05-21',