mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
Merge pull request #6203 from tk0miya/refactor_citations
Add CitationDomain for citation processing
This commit is contained in:
6
CHANGES
6
CHANGES
@@ -33,11 +33,17 @@ Deprecated
|
||||
* ``sphinx.directives.TabularColumns``
|
||||
* ``sphinx.directives.TocTree``
|
||||
* ``sphinx.directives.VersionChange``
|
||||
* ``sphinx.domains.std.StandardDomain._resolve_citation_xref()``
|
||||
* ``sphinx.domains.std.StandardDomain.note_citations()``
|
||||
* ``sphinx.domains.std.StandardDomain.note_citation_refs()``
|
||||
* ``sphinx.domains.std.StandardDomain.note_labels()``
|
||||
* ``sphinx.environment.NoUri``
|
||||
* ``sphinx.ext.autodoc.importer.MockFinder``
|
||||
* ``sphinx.ext.autodoc.importer.MockLoader``
|
||||
* ``sphinx.ext.autodoc.importer.mock()``
|
||||
* ``sphinx.ext.autosummary.autolink_role()``
|
||||
* ``sphinx.transforms.CitationReferences``
|
||||
* ``sphinx.transforms.SmartQuotesSkipper``
|
||||
* ``sphinx.util.docfields.DocFieldTransformer.preprocess_fieldtypes()``
|
||||
* ``sphinx.util.node.find_source_node()``
|
||||
* ``sphinx.util.i18n.find_catalog()``
|
||||
|
||||
@@ -116,6 +116,26 @@ The following is a list of deprecated interfaces.
|
||||
- 4.0
|
||||
- ``sphinx.directives.other.VersionChange``
|
||||
|
||||
* - ``sphinx.domains.std.StandardDomain._resolve_citation_xref()``
|
||||
- 2.1
|
||||
- 4.0
|
||||
- ``sphinx.domains.citation.CitationDomain.resolve_xref()``
|
||||
|
||||
* - ``sphinx.domains.std.StandardDomain.note_citations()``
|
||||
- 2.1
|
||||
- 4.0
|
||||
- ``sphinx.domains.citation.CitationDomain.note_citation()``
|
||||
|
||||
* - ``sphinx.domains.std.StandardDomain.note_citation_refs()``
|
||||
- 2.1
|
||||
- 4.0
|
||||
- ``sphinx.domains.citation.CitationDomain.note_citation_reference()``
|
||||
|
||||
* - ``sphinx.domains.std.StandardDomain.note_labels()``
|
||||
- 2.1
|
||||
- 4.0
|
||||
- ``sphinx.domains.std.StandardDomain.process_doc()``
|
||||
|
||||
* - ``sphinx.environment.NoUri``
|
||||
- 2.1
|
||||
- 4.0
|
||||
@@ -141,6 +161,16 @@ The following is a list of deprecated interfaces.
|
||||
- 4.0
|
||||
- ``sphinx.ext.autosummary.AutoLink``
|
||||
|
||||
* - ``sphinx.transforms.CitationReferences``
|
||||
- 2.1
|
||||
- 4.0
|
||||
- ``sphinx.domains.citation.CitationReferenceTransform``
|
||||
|
||||
* - ``sphinx.transforms.SmartQuotesSkipper``
|
||||
- 2.1
|
||||
- 4.0
|
||||
- ``sphinx.domains.citation.CitationDefinitionTransform``
|
||||
|
||||
* - ``sphinx.util.docfields.DocFieldTransformer.preprocess_fieldtypes()``
|
||||
- 2.1
|
||||
- 4.0
|
||||
|
||||
@@ -76,6 +76,7 @@ builtin_extensions = (
|
||||
'sphinx.config',
|
||||
'sphinx.domains.c',
|
||||
'sphinx.domains.changeset',
|
||||
'sphinx.domains.citation',
|
||||
'sphinx.domains.cpp',
|
||||
'sphinx.domains.javascript',
|
||||
'sphinx.domains.math',
|
||||
|
||||
@@ -16,6 +16,7 @@ from sphinx import addnodes
|
||||
from sphinx.builders.latex.nodes import (
|
||||
captioned_literal_block, footnotemark, footnotetext, math_reference, thebibliography
|
||||
)
|
||||
from sphinx.domains.citation import CitationDomain
|
||||
from sphinx.transforms import SphinxTransform
|
||||
from sphinx.transforms.post_transforms import SphinxPostTransform
|
||||
from sphinx.util.nodes import NodeMatcher
|
||||
@@ -545,10 +546,10 @@ class CitationReferenceTransform(SphinxPostTransform):
|
||||
|
||||
def run(self, **kwargs):
|
||||
# type: (Any) -> None
|
||||
matcher = NodeMatcher(addnodes.pending_xref, refdomain='std', reftype='citation')
|
||||
citations = self.env.get_domain('std').data['citations']
|
||||
domain = cast(CitationDomain, self.env.get_domain('citation'))
|
||||
matcher = NodeMatcher(addnodes.pending_xref, refdomain='citation', reftype='ref')
|
||||
for node in self.document.traverse(matcher): # type: addnodes.pending_xref
|
||||
docname, labelid, _ = citations.get(node['reftarget'], ('', '', 0))
|
||||
docname, labelid, _ = domain.citations.get(node['reftarget'], ('', '', 0))
|
||||
if docname:
|
||||
citation_ref = nodes.citation_reference('', '', *node.children,
|
||||
docname=docname, refname=labelid)
|
||||
|
||||
167
sphinx/domains/citation.py
Normal file
167
sphinx/domains/citation.py
Normal file
@@ -0,0 +1,167 @@
|
||||
"""
|
||||
sphinx.domains.citation
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The citation domain.
|
||||
|
||||
:copyright: Copyright 2007-2019 by the Sphinx team, see AUTHORS.
|
||||
:license: BSD, see LICENSE for details.
|
||||
"""
|
||||
|
||||
from typing import cast
|
||||
|
||||
from docutils import nodes
|
||||
|
||||
from sphinx import addnodes
|
||||
from sphinx.domains import Domain
|
||||
from sphinx.locale import __
|
||||
from sphinx.transforms import SphinxTransform
|
||||
from sphinx.util import logging
|
||||
from sphinx.util.nodes import copy_source_info, make_refnode
|
||||
|
||||
if False:
|
||||
# For type annotation
|
||||
from typing import Any, Dict, List, Set, Tuple, Union # NOQA
|
||||
from sphinx.application import Sphinx # NOQA
|
||||
from sphinx.builders import Builder # NOQA
|
||||
from sphinx.environment import BuildEnvironment # NOQA
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CitationDomain(Domain):
|
||||
"""Domain for citations."""
|
||||
|
||||
name = 'citation'
|
||||
label = 'citation'
|
||||
|
||||
dangling_warnings = {
|
||||
'ref': 'citation not found: %(target)s',
|
||||
}
|
||||
|
||||
@property
|
||||
def citations(self):
|
||||
# type: () -> Dict[str, Tuple[str, str, int]]
|
||||
return self.data.setdefault('citations', {})
|
||||
|
||||
@property
|
||||
def citation_refs(self):
|
||||
# type: () -> Dict[str, Set[str]]
|
||||
return self.data.setdefault('citation_refs', {})
|
||||
|
||||
def clear_doc(self, docname):
|
||||
# type: (str) -> None
|
||||
for key, (fn, _l, lineno) in list(self.citations.items()):
|
||||
if fn == docname:
|
||||
del self.citations[key]
|
||||
for key, docnames in list(self.citation_refs.items()):
|
||||
if docnames == {docname}:
|
||||
del self.citation_refs[key]
|
||||
elif docname in docnames:
|
||||
docnames.remove(docname)
|
||||
|
||||
def merge_domaindata(self, docnames, otherdata):
|
||||
# type: (List[str], Dict) -> None
|
||||
# XXX duplicates?
|
||||
for key, data in otherdata['citations'].items():
|
||||
if data[0] in docnames:
|
||||
self.citations[key] = data
|
||||
for key, data in otherdata['citation_refs'].items():
|
||||
citation_refs = self.citation_refs.setdefault(key, set())
|
||||
for docname in data:
|
||||
if docname in docnames:
|
||||
citation_refs.add(docname)
|
||||
|
||||
def note_citation(self, node):
|
||||
# type: (nodes.citation) -> None
|
||||
label = node[0].astext()
|
||||
if label in self.citations:
|
||||
path = self.env.doc2path(self.citations[label][0])
|
||||
logger.warning(__('duplicate citation %s, other instance in %s'), label, path,
|
||||
location=node, type='ref', subtype='citation')
|
||||
self.citations[label] = (node['docname'], node['ids'][0], node.line)
|
||||
|
||||
def note_citation_reference(self, node):
|
||||
# type: (addnodes.pending_xref) -> None
|
||||
docnames = self.citation_refs.setdefault(node['reftarget'], set())
|
||||
docnames.add(self.env.docname)
|
||||
|
||||
def check_consistency(self):
|
||||
# type: () -> None
|
||||
for name, (docname, labelid, lineno) in self.citations.items():
|
||||
if name not in self.citation_refs:
|
||||
logger.warning(__('Citation [%s] is not referenced.'), name,
|
||||
type='ref', subtype='citation', location=(docname, lineno))
|
||||
|
||||
def resolve_xref(self, env, fromdocname, builder, typ, target, node, contnode):
|
||||
# type: (BuildEnvironment, str, Builder, str, str, addnodes.pending_xref, nodes.Element) -> nodes.Element # NOQA
|
||||
docname, labelid, lineno = self.citations.get(target, ('', '', 0))
|
||||
if not docname:
|
||||
return None
|
||||
|
||||
return make_refnode(builder, fromdocname, docname,
|
||||
labelid, contnode)
|
||||
|
||||
def resolve_any_xref(self, env, fromdocname, builder, target, node, contnode):
|
||||
# type: (BuildEnvironment, str, Builder, str, addnodes.pending_xref, nodes.Element) -> List[Tuple[str, nodes.Element]] # NOQA
|
||||
refnode = self.resolve_xref(env, fromdocname, builder, 'ref', target, node, contnode)
|
||||
if refnode is None:
|
||||
return []
|
||||
else:
|
||||
return [('ref', refnode)]
|
||||
|
||||
|
||||
class CitationDefinitionTransform(SphinxTransform):
|
||||
"""Mark citation definition labels as not smartquoted."""
|
||||
default_priority = 619
|
||||
|
||||
def apply(self, **kwargs):
|
||||
# type: (Any) -> None
|
||||
domain = cast(CitationDomain, self.env.get_domain('citation'))
|
||||
for node in self.document.traverse(nodes.citation):
|
||||
# register citation node to domain
|
||||
node['docname'] = self.env.docname
|
||||
domain.note_citation(node)
|
||||
|
||||
# mark citation labels as not smartquoted
|
||||
label = cast(nodes.label, node[0])
|
||||
label['support_smartquotes'] = False
|
||||
|
||||
|
||||
class CitationReferenceTransform(SphinxTransform):
|
||||
"""
|
||||
Replace citation references by pending_xref nodes before the default
|
||||
docutils transform tries to resolve them.
|
||||
"""
|
||||
default_priority = 619
|
||||
|
||||
def apply(self, **kwargs):
|
||||
# type: (Any) -> None
|
||||
domain = cast(CitationDomain, self.env.get_domain('citation'))
|
||||
for node in self.document.traverse(nodes.citation_reference):
|
||||
target = node.astext()
|
||||
ref = addnodes.pending_xref(target, refdomain='citation', reftype='ref',
|
||||
reftarget=target, refwarn=True,
|
||||
support_smartquotes=False,
|
||||
ids=node["ids"],
|
||||
classes=node.get('classes', []))
|
||||
ref += nodes.inline(target, '[%s]' % target)
|
||||
copy_source_info(node, ref)
|
||||
node.replace_self(ref)
|
||||
|
||||
# register reference node to domain
|
||||
domain.note_citation_reference(ref)
|
||||
|
||||
|
||||
def setup(app):
|
||||
# type: (Sphinx) -> Dict[str, Any]
|
||||
app.add_domain(CitationDomain)
|
||||
app.add_transform(CitationDefinitionTransform)
|
||||
app.add_transform(CitationReferenceTransform)
|
||||
|
||||
return {
|
||||
'version': 'builtin',
|
||||
'env_version': 1,
|
||||
'parallel_read_safe': True,
|
||||
'parallel_write_safe': True,
|
||||
}
|
||||
@@ -19,7 +19,7 @@ from docutils.parsers.rst import directives
|
||||
from docutils.statemachine import StringList
|
||||
|
||||
from sphinx import addnodes
|
||||
from sphinx.deprecation import RemovedInSphinx30Warning
|
||||
from sphinx.deprecation import RemovedInSphinx30Warning, RemovedInSphinx40Warning
|
||||
from sphinx.directives import ObjectDescription
|
||||
from sphinx.domains import Domain, ObjType
|
||||
from sphinx.errors import NoUri
|
||||
@@ -499,8 +499,6 @@ class StandardDomain(Domain):
|
||||
initial_data = {
|
||||
'progoptions': {}, # (program, name) -> docname, labelid
|
||||
'objects': {}, # (type, name) -> docname, labelid
|
||||
'citations': {}, # citation_name -> docname, labelid, lineno
|
||||
'citation_refs': {}, # citation_name -> list of docnames
|
||||
'labels': { # labelname -> docname, labelid, sectionname
|
||||
'genindex': ('genindex', '', _('Index')),
|
||||
'modindex': ('py-modindex', '', _('Module Index')),
|
||||
@@ -521,7 +519,6 @@ class StandardDomain(Domain):
|
||||
'keyword': 'unknown keyword: %(target)s',
|
||||
'doc': 'unknown document: %(target)s',
|
||||
'option': 'unknown option: %(target)s',
|
||||
'citation': 'citation not found: %(target)s',
|
||||
}
|
||||
|
||||
enumerable_nodes = { # node_class -> (figtype, title_getter)
|
||||
@@ -568,14 +565,6 @@ class StandardDomain(Domain):
|
||||
for key, (fn, _l) in list(self.objects.items()):
|
||||
if fn == docname:
|
||||
del self.objects[key]
|
||||
for key, (fn, _l, lineno) in list(self.data['citations'].items()):
|
||||
if fn == docname:
|
||||
del self.data['citations'][key]
|
||||
for key, docnames in list(self.data['citation_refs'].items()):
|
||||
if docnames == [docname]:
|
||||
del self.data['citation_refs'][key]
|
||||
elif docname in docnames:
|
||||
docnames.remove(docname)
|
||||
for key, (fn, _l, _l) in list(self.labels.items()):
|
||||
if fn == docname:
|
||||
del self.labels[key]
|
||||
@@ -592,14 +581,6 @@ class StandardDomain(Domain):
|
||||
for key, data in otherdata['objects'].items():
|
||||
if data[0] in docnames:
|
||||
self.objects[key] = data
|
||||
for key, data in otherdata['citations'].items():
|
||||
if data[0] in docnames:
|
||||
self.data['citations'][key] = data
|
||||
for key, data in otherdata['citation_refs'].items():
|
||||
citation_refs = self.data['citation_refs'].setdefault(key, [])
|
||||
for docname in data:
|
||||
if docname in docnames:
|
||||
citation_refs.append(docname)
|
||||
for key, data in otherdata['labels'].items():
|
||||
if data[0] in docnames:
|
||||
self.labels[key] = data
|
||||
@@ -608,31 +589,6 @@ class StandardDomain(Domain):
|
||||
self.anonlabels[key] = data
|
||||
|
||||
def process_doc(self, env, docname, document):
|
||||
# type: (BuildEnvironment, str, nodes.document) -> None
|
||||
self.note_citations(env, docname, document)
|
||||
self.note_citation_refs(env, docname, document)
|
||||
self.note_labels(env, docname, document)
|
||||
|
||||
def note_citations(self, env, docname, document):
|
||||
# type: (BuildEnvironment, str, nodes.document) -> None
|
||||
for node in document.traverse(nodes.citation):
|
||||
node['docname'] = docname
|
||||
label = cast(nodes.label, node[0]).astext()
|
||||
if label in self.data['citations']:
|
||||
path = env.doc2path(self.data['citations'][label][0])
|
||||
logger.warning(__('duplicate citation %s, other instance in %s'), label, path,
|
||||
location=node, type='ref', subtype='citation')
|
||||
self.data['citations'][label] = (docname, node['ids'][0], node.line)
|
||||
|
||||
def note_citation_refs(self, env, docname, document):
|
||||
# type: (BuildEnvironment, str, nodes.document) -> None
|
||||
for node in document.traverse(addnodes.pending_xref):
|
||||
if node['refdomain'] == 'std' and node['reftype'] == 'citation':
|
||||
label = node['reftarget']
|
||||
citation_refs = self.data['citation_refs'].setdefault(label, [])
|
||||
citation_refs.append(docname)
|
||||
|
||||
def note_labels(self, env, docname, document):
|
||||
# type: (BuildEnvironment, str, nodes.document) -> None
|
||||
for name, explicit in document.nametypes.items():
|
||||
if not explicit:
|
||||
@@ -682,14 +638,6 @@ class StandardDomain(Domain):
|
||||
# type: (str, str, str, str) -> None
|
||||
self.progoptions[program, name] = (docname, labelid)
|
||||
|
||||
def check_consistency(self):
|
||||
# type: () -> None
|
||||
for name, (docname, labelid, lineno) in self.data['citations'].items():
|
||||
if name not in self.data['citation_refs']:
|
||||
logger.warning(__('Citation [%s] is not referenced.'), name,
|
||||
type='ref', subtype='citation',
|
||||
location=(docname, lineno))
|
||||
|
||||
def build_reference_node(self, fromdocname, builder, docname, labelid,
|
||||
sectname, rolename, **options):
|
||||
# type: (str, Builder, str, str, str, str, Any) -> nodes.Element
|
||||
@@ -728,7 +676,10 @@ class StandardDomain(Domain):
|
||||
elif typ == 'option':
|
||||
resolver = self._resolve_option_xref
|
||||
elif typ == 'citation':
|
||||
resolver = self._resolve_citation_xref
|
||||
warnings.warn('pending_xref(domain=std, type=citation) is deprecated: %r' % node,
|
||||
RemovedInSphinx40Warning)
|
||||
domain = env.get_domain('citation')
|
||||
return domain.resolve_xref(env, fromdocname, builder, typ, target, node, contnode)
|
||||
else:
|
||||
resolver = self._resolve_obj_xref
|
||||
|
||||
@@ -860,6 +811,8 @@ class StandardDomain(Domain):
|
||||
|
||||
def _resolve_citation_xref(self, env, fromdocname, builder, typ, target, node, contnode):
|
||||
# type: (BuildEnvironment, str, Builder, str, str, addnodes.pending_xref, nodes.Element) -> nodes.Element # NOQA
|
||||
warnings.warn('StandardDomain._resolve_citation_xref() is deprecated.',
|
||||
RemovedInSphinx30Warning)
|
||||
docname, labelid, lineno = self.data['citations'].get(target, ('', '', 0))
|
||||
if not docname:
|
||||
if 'ids' in node:
|
||||
@@ -1027,6 +980,21 @@ class StandardDomain(Domain):
|
||||
else:
|
||||
return None
|
||||
|
||||
def note_citations(self, env, docname, document):
|
||||
# type: (BuildEnvironment, str, nodes.document) -> None
|
||||
warnings.warn('StandardDomain.note_citations() is deprecated.',
|
||||
RemovedInSphinx40Warning)
|
||||
|
||||
def note_citation_refs(self, env, docname, document):
|
||||
# type: (BuildEnvironment, str, nodes.document) -> None
|
||||
warnings.warn('StandardDomain.note_citation_refs() is deprecated.',
|
||||
RemovedInSphinx40Warning)
|
||||
|
||||
def note_labels(self, env, docname, document):
|
||||
# type: (BuildEnvironment, str, nodes.document) -> None
|
||||
warnings.warn('StandardDomain.note_labels() is deprecated.',
|
||||
RemovedInSphinx40Warning)
|
||||
|
||||
|
||||
def setup(app):
|
||||
# type: (Sphinx) -> Dict[str, Any]
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
"""
|
||||
|
||||
import re
|
||||
from typing import cast
|
||||
|
||||
from docutils import nodes
|
||||
from docutils.transforms import Transform, Transformer
|
||||
@@ -19,13 +18,12 @@ from docutils.utils import normalize_language_tag
|
||||
from docutils.utils.smartquotes import smartchars
|
||||
|
||||
from sphinx import addnodes
|
||||
from sphinx.deprecation import RemovedInSphinx40Warning, deprecated_alias
|
||||
from sphinx.locale import _, __
|
||||
from sphinx.util import logging
|
||||
from sphinx.util.docutils import new_document
|
||||
from sphinx.util.i18n import format_date
|
||||
from sphinx.util.nodes import (
|
||||
NodeMatcher, apply_source_workaround, copy_source_info, is_smartquotable
|
||||
)
|
||||
from sphinx.util.nodes import NodeMatcher, apply_source_workaround, is_smartquotable
|
||||
|
||||
if False:
|
||||
# For type annotation
|
||||
@@ -200,39 +198,6 @@ class SortIds(SphinxTransform):
|
||||
node['ids'] = node['ids'][1:] + [node['ids'][0]]
|
||||
|
||||
|
||||
class SmartQuotesSkipper(SphinxTransform):
|
||||
"""Mark specific nodes as not smartquoted."""
|
||||
default_priority = 619
|
||||
|
||||
def apply(self, **kwargs):
|
||||
# type: (Any) -> None
|
||||
# citation labels
|
||||
for node in self.document.traverse(nodes.citation):
|
||||
label = cast(nodes.label, node[0])
|
||||
label['support_smartquotes'] = False
|
||||
|
||||
|
||||
class CitationReferences(SphinxTransform):
|
||||
"""
|
||||
Replace citation references by pending_xref nodes before the default
|
||||
docutils transform tries to resolve them.
|
||||
"""
|
||||
default_priority = 619
|
||||
|
||||
def apply(self, **kwargs):
|
||||
# type: (Any) -> None
|
||||
for node in self.document.traverse(nodes.citation_reference):
|
||||
target = node.astext()
|
||||
ref = addnodes.pending_xref(target, refdomain='std', reftype='citation',
|
||||
reftarget=target, refwarn=True,
|
||||
support_smartquotes=False,
|
||||
ids=node["ids"],
|
||||
classes=node.get('classes', []))
|
||||
ref += nodes.inline(target, '[%s]' % target)
|
||||
copy_source_info(node, ref)
|
||||
node.replace_self(ref)
|
||||
|
||||
|
||||
TRANSLATABLE_NODES = {
|
||||
'literal-block': nodes.literal_block,
|
||||
'doctest-block': nodes.doctest_block,
|
||||
@@ -440,12 +405,22 @@ class ManpageLink(SphinxTransform):
|
||||
node.attributes.update(info)
|
||||
|
||||
|
||||
from sphinx.domains.citation import ( # NOQA
|
||||
CitationDefinitionTransform, CitationReferenceTransform
|
||||
)
|
||||
|
||||
deprecated_alias('sphinx.transforms',
|
||||
{
|
||||
'CitationReferences': CitationReferenceTransform,
|
||||
'SmartQuotesSkipper': CitationDefinitionTransform,
|
||||
},
|
||||
RemovedInSphinx40Warning)
|
||||
|
||||
|
||||
def setup(app):
|
||||
# type: (Sphinx) -> Dict[str, Any]
|
||||
app.add_transform(ApplySourceWorkaround)
|
||||
app.add_transform(ExtraTranslatableNodes)
|
||||
app.add_transform(SmartQuotesSkipper)
|
||||
app.add_transform(CitationReferences)
|
||||
app.add_transform(DefaultSubstitutions)
|
||||
app.add_transform(MoveModuleTargets)
|
||||
app.add_transform(HandleCodeBlocks)
|
||||
|
||||
Reference in New Issue
Block a user