diff --git a/sphinx/environment/__init__.py b/sphinx/environment/__init__.py index 251a88589..17f9667a1 100644 --- a/sphinx/environment/__init__.py +++ b/sphinx/environment/__init__.py @@ -19,13 +19,11 @@ import warnings from os import path from copy import copy from collections import defaultdict -from contextlib import contextmanager -from six import BytesIO, itervalues, class_types, next, iteritems +from six import BytesIO, itervalues, class_types, next from six.moves import cPickle as pickle -from docutils.utils import Reporter, get_source_line, normalize_language_tag -from docutils.utils.smartquotes import smartchars +from docutils.utils import Reporter, get_source_line from docutils.frontend import OptionParser from sphinx import addnodes, versioning @@ -41,7 +39,7 @@ from sphinx.util.matching import compile_matchers from sphinx.util.parallel import ParallelTasks, parallel_available, make_chunks from sphinx.util.websupport import is_commentable from sphinx.errors import SphinxError, ExtensionError -from sphinx.transforms import SphinxTransformer, SphinxSmartQuotes +from sphinx.transforms import SphinxTransformer from sphinx.deprecation import RemovedInSphinx20Warning from sphinx.environment.adapters.indexentries import IndexEntries from sphinx.environment.adapters.toctree import TocTree @@ -84,22 +82,6 @@ versioning_conditions = { } # type: Dict[unicode, Union[bool, Callable]] -@contextmanager -def sphinx_smartquotes_action(env): - # type: (BuildEnvironment) -> Generator - if not hasattr(SphinxSmartQuotes, 'smartquotes_action'): - # less than docutils-0.14 - yield - else: - # docutils-0.14 or above - try: - original = SphinxSmartQuotes.smartquotes_action - SphinxSmartQuotes.smartquotes_action = env.config.smartquotes_action - yield - finally: - SphinxSmartQuotes.smartquotes_action = original - - class NoUri(Exception): """Raised by get_relative_uri if there is no URI available.""" pass @@ -602,8 +584,7 @@ class BuildEnvironment(object): # remove all inventory entries for that file app.emit('env-purge-doc', self, docname) self.clear_doc(docname) - with sphinx_smartquotes_action(self): - self.read_doc(docname, app) + self.read_doc(docname, app) def _read_parallel(self, docnames, app, nproc): # type: (List[unicode], Sphinx, int) -> None @@ -615,9 +596,8 @@ class BuildEnvironment(object): def read_process(docs): # type: (List[unicode]) -> unicode self.app = app - with sphinx_smartquotes_action(self): - for docname in docs: - self.read_doc(docname, app) + for docname in docs: + self.read_doc(docname, app) # allow pickling self to send it back return BuildEnvironment.dumps(self) @@ -662,29 +642,10 @@ class BuildEnvironment(object): self.config.trim_footnote_reference_space self.settings['gettext_compact'] = self.config.gettext_compact - language = self.config.language or 'en' - self.settings['language_code'] = language - if 'smart_quotes' not in self.settings: - self.settings['smart_quotes'] = self.config.smartquotes + self.settings['language_code'] = self.config.language or 'en' - # some conditions exclude smart quotes, overriding smart_quotes - for valname, vallist in iteritems(self.config.smartquotes_excludes): - if valname == 'builders': - # this will work only for checking first build target - if self.app.builder.name in vallist: - self.settings['smart_quotes'] = False - break - elif valname == 'languages': - if self.config.language in vallist: - self.settings['smart_quotes'] = False - break - - # confirm selected language supports smart_quotes or not - for tag in normalize_language_tag(language): - if tag in smartchars.quotes: - break - else: - self.settings['smart_quotes'] = False + # Allow to disable by 3rd party extension (workaround) + self.settings.setdefault('smart_quotes', True) def read_doc(self, docname, app=None): # type: (unicode, Sphinx) -> None diff --git a/sphinx/io.py b/sphinx/io.py index 3c32c167c..6943a78d0 100644 --- a/sphinx/io.py +++ b/sphinx/io.py @@ -19,6 +19,7 @@ from docutils.writers import UnfilteredWriter from six import text_type from typing import Any, Union # NOQA +from sphinx.transforms import SphinxTransformer from sphinx.transforms import ( ApplySourceWorkaround, ExtraTranslatableNodes, CitationReferences, DefaultSubstitutions, MoveModuleTargets, HandleCodeBlocks, SortIds, @@ -56,6 +57,11 @@ class SphinxBaseReader(standalone.Reader): This replaces reporter by Sphinx's on generating document. """ + def __init__(self, app, *args, **kwargs): + # type: (Sphinx, Any, Any) -> None + self.env = app.env + standalone.Reader.__init__(self, *args, **kwargs) + def get_transforms(self): # type: () -> List[Transform] return standalone.Reader.get_transforms(self) + self.transforms @@ -66,9 +72,16 @@ class SphinxBaseReader(standalone.Reader): for logging. """ document = standalone.Reader.new_document(self) + + # substitute transformer + document.transformer = SphinxTransformer(document) + document.transformer.set_environment(self.env) + + # substitute reporter reporter = document.reporter document.reporter = LoggingReporter.from_reporter(reporter) document.reporter.set_source(self.source) + return document @@ -80,20 +93,14 @@ class SphinxStandaloneReader(SphinxBaseReader): Locale, CitationReferences, DefaultSubstitutions, MoveModuleTargets, HandleCodeBlocks, AutoNumbering, AutoIndexUpgrader, SortIds, RemoveTranslatableInline, PreserveTranslatableMessages, FilterSystemMessages, - RefOnlyBulletListTransform, UnreferencedFootnotesDetector, ManpageLink + RefOnlyBulletListTransform, UnreferencedFootnotesDetector, SphinxSmartQuotes, + ManpageLink ] # type: List[Transform] def __init__(self, app, *args, **kwargs): # type: (Sphinx, Any, Any) -> None self.transforms = self.transforms + app.registry.get_transforms() - self.smart_quotes = app.env.settings['smart_quotes'] - SphinxBaseReader.__init__(self, *args, **kwargs) # type: ignore - - def get_transforms(self): - transforms = SphinxBaseReader.get_transforms(self) - if self.smart_quotes: - transforms.append(SphinxSmartQuotes) - return transforms + SphinxBaseReader.__init__(self, app, *args, **kwargs) class SphinxI18nReader(SphinxBaseReader): diff --git a/sphinx/transforms/__init__.py b/sphinx/transforms/__init__.py index ceb8de364..a0bc25c25 100644 --- a/sphinx/transforms/__init__.py +++ b/sphinx/transforms/__init__.py @@ -14,8 +14,9 @@ import re from docutils import nodes from docutils.transforms import Transform, Transformer from docutils.transforms.parts import ContentsFilter -from docutils.utils import new_document from docutils.transforms.universal import SmartQuotes +from docutils.utils import new_document, normalize_language_tag +from docutils.utils.smartquotes import smartchars from sphinx import addnodes from sphinx.locale import _ @@ -335,12 +336,54 @@ class SphinxContentsFilter(ContentsFilter): raise nodes.SkipNode -class SphinxSmartQuotes(SmartQuotes): +class SphinxSmartQuotes(SmartQuotes, SphinxTransform): """ Customized SmartQuotes to avoid transform for some extra node types. refs: sphinx.parsers.RSTParser """ + def apply(self): + # type: () -> None + if not self.is_available(): + return + + SmartQuotes.apply(self) + + def is_available(self): + # type: () -> bool + builders = self.config.smartquotes_excludes.get('builders', []) + languages = self.config.smartquotes_excludes.get('languages', []) + + if self.document.settings.smart_quotes is False: + # disabled by 3rd party extension (workaround) + return False + elif self.config.smartquotes is False: + # disabled by confval smartquotes + return False + elif self.app.builder.name in builders: + # disabled by confval smartquotes_excludes['builders'] + return False + elif self.config.language in languages: + # disabled by confval smartquotes_excludes['languages'] + return False + + # confirm selected language supports smart_quotes or not + language = self.env.settings['language_code'] + for tag in normalize_language_tag(language): + if tag in smartchars.quotes: + return True + else: + return False + + @property + def smartquotes_action(self): + # type: () -> unicode + """A smartquotes_action setting for SmartQuotes. + + Users can change this setting through :confval:`smartquotes_action`. + """ + return self.config.smartquotes_action + def get_tokens(self, txtnodes): # A generator that yields ``(texttype, nodetext)`` tuples for a list # of "Text" nodes (interface to ``smartquotes.educate_tokens()``). diff --git a/sphinx/transforms/i18n.py b/sphinx/transforms/i18n.py index 5ae33d86a..db67aae97 100644 --- a/sphinx/transforms/i18n.py +++ b/sphinx/transforms/i18n.py @@ -50,7 +50,7 @@ def publish_msgstr(app, source, source_path, source_line, config, settings): :rtype: docutils.nodes.document """ from sphinx.io import SphinxI18nReader - reader = SphinxI18nReader() + reader = SphinxI18nReader(app) reader.set_lineno_for_reporter(source_line) parser = app.registry.create_source_parser(app, '') doc = reader.read(