mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
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:
parent
710ddb3880
commit
edc7f30b9c
@ -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):
|
||||||
|
@ -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,15 +219,9 @@ 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 HTML5Translator
|
||||||
return SmartyPantsHTML5Translator
|
|
||||||
else:
|
|
||||||
return HTML5Translator
|
|
||||||
else:
|
else:
|
||||||
if self.config.html_use_smartypants:
|
return HTMLTranslator
|
||||||
return SmartyPantsHTMLTranslator
|
|
||||||
else:
|
|
||||||
return HTMLTranslator
|
|
||||||
|
|
||||||
def get_outdated_docs(self):
|
def get_outdated_docs(self):
|
||||||
# type: () -> Iterator[unicode]
|
# type: () -> Iterator[unicode]
|
||||||
|
@ -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
|
||||||
|
@ -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('"', '"')
|
|
||||||
t = educate_dashes_oldschool(t)
|
|
||||||
t = educate_quotes(t) # type: ignore
|
|
||||||
t = t.replace('"', '"')
|
|
||||||
return t
|
|
||||||
|
|
||||||
|
|
||||||
# Constants for quote education.
|
|
||||||
punct_class = r"""[!"#\$\%'()*+,-.\/:;<=>?\@\[\\\]\^_`{|}~]"""
|
|
||||||
end_of_word_class = r"""[\s.,;:!?)]"""
|
|
||||||
close_class = r"""[^\ \t\r\n\[\{\(\-]"""
|
|
||||||
dec_dashes = 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:
|
|
||||||
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
|
|
||||||
  | # 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
|
|
||||||
  | # 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: “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("’", s)
|
|
||||||
s = double_quote_start_re.sub("”", 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("“‘", s)
|
|
||||||
s = single_quote_sets_re.sub("‘“", s)
|
|
||||||
|
|
||||||
# Special case for decade abbreviations (the '80s):
|
|
||||||
s = decade_abbr_re.sub("’", s)
|
|
||||||
|
|
||||||
s = opening_single_quotes_regex.sub(r"\1‘", s)
|
|
||||||
s = closing_single_quotes_regex.sub(r"\1’", s)
|
|
||||||
s = closing_single_quotes_regex_2.sub(r"\1’\2", s)
|
|
||||||
|
|
||||||
# Any remaining single quotes should be opening ones:
|
|
||||||
s = s.replace("'", "‘")
|
|
||||||
|
|
||||||
s = opening_double_quotes_regex.sub(r"\1“", s)
|
|
||||||
s = closing_double_quotes_regex.sub(r"”", s)
|
|
||||||
s = closing_double_quotes_regex_2.sub(r"\1”", s)
|
|
||||||
|
|
||||||
# Any remaining quotes should be opening ones.
|
|
||||||
return s.replace('"', "“")
|
|
||||||
|
|
||||||
|
|
||||||
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: “Isn't this fun?”
|
|
||||||
"""
|
|
||||||
return s.replace("``", "“").replace("''", "”")
|
|
||||||
|
|
||||||
|
|
||||||
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: ‘Isn’t this fun?’
|
|
||||||
"""
|
|
||||||
return s.replace('`', "‘").replace("'", "’")
|
|
||||||
|
|
||||||
|
|
||||||
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('---', "—").replace('--', "–")
|
|
||||||
|
|
||||||
|
|
||||||
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('---', "–").replace('--', "—")
|
|
||||||
|
|
||||||
|
|
||||||
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…?
|
|
||||||
"""
|
|
||||||
return s.replace('...', "…").replace('. . .', "…")
|
|
@ -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
|
|
||||||
|
@ -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
|
|
||||||
|
@ -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):
|
||||||
|
@ -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'
|
||||||
|
|
||||||
|
|
||||||
|
@ -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 Tests’s 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 Tests’s documentation!")
|
||||||
assert toc[0].findall("./xhtml:ol") == []
|
assert toc[0].findall("./xhtml:ol") == []
|
||||||
|
|
||||||
# nav.xhtml / nested toc
|
# nav.xhtml / nested toc
|
||||||
|
@ -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 Tests’s 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 Tests’s 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 Tests’s 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]))
|
||||||
|
@ -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>“John”</p>',
|
u'<p>“John”</p>',
|
||||||
r'\sphinxquotedblleft{}John\sphinxquotedblright{}',
|
u"“John”",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
# ... but not in literal text
|
# ... but not in literal text
|
||||||
|
@ -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 else’s '
|
||||||
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',
|
||||||
|
Loading…
Reference in New Issue
Block a user