diff --git a/CHANGES b/CHANGES index d9d5afd0e..ef8b0de04 100644 --- a/CHANGES +++ b/CHANGES @@ -31,6 +31,7 @@ Deprecated * ``sphinx.directives.TabularColumns`` * ``sphinx.directives.TocTree`` * ``sphinx.directives.VersionChange`` +* ``sphinx.domains.std.StandardDomain._resolve_citation_xref()`` * ``sphinx.environment.NoUri`` * ``sphinx.ext.autodoc.importer.MockFinder`` * ``sphinx.ext.autodoc.importer.MockLoader`` diff --git a/doc/extdev/deprecated.rst b/doc/extdev/deprecated.rst index 9c759f507..221d7e735 100644 --- a/doc/extdev/deprecated.rst +++ b/doc/extdev/deprecated.rst @@ -116,6 +116,11 @@ 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.environment.NoUri`` - 2.1 - 4.0 diff --git a/sphinx/builders/latex/transforms.py b/sphinx/builders/latex/transforms.py index 746446fbc..6381780ae 100644 --- a/sphinx/builders/latex/transforms.py +++ b/sphinx/builders/latex/transforms.py @@ -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) diff --git a/sphinx/domains/citation.py b/sphinx/domains/citation.py index 7ac2944dd..2bb49def9 100644 --- a/sphinx/domains/citation.py +++ b/sphinx/domains/citation.py @@ -13,13 +13,102 @@ 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.nodes import copy_source_info +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 # NOQA + 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): @@ -28,7 +117,13 @@ class CitationDefinitionTransform(SphinxTransform): 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 @@ -42,9 +137,10 @@ class CitationReferenceTransform(SphinxTransform): 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='std', reftype='citation', + ref = addnodes.pending_xref(target, refdomain='citation', reftype='ref', reftarget=target, refwarn=True, support_smartquotes=False, ids=node["ids"], @@ -53,14 +149,19 @@ class CitationReferenceTransform(SphinxTransform): 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, } diff --git a/sphinx/domains/std.py b/sphinx/domains/std.py index 2cfb4ceaa..188a20f0c 100644 --- a/sphinx/domains/std.py +++ b/sphinx/domains/std.py @@ -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 @@ -496,8 +496,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')), @@ -518,7 +516,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) @@ -544,14 +541,6 @@ class StandardDomain(Domain): for key, (fn, _l) in list(self.data['objects'].items()): if fn == docname: del self.data['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.data['labels'].items()): if fn == docname: del self.data['labels'][key] @@ -568,14 +557,6 @@ class StandardDomain(Domain): for key, data in otherdata['objects'].items(): if data[0] in docnames: self.data['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.data['labels'][key] = data @@ -584,31 +565,6 @@ class StandardDomain(Domain): self.data['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 labels, anonlabels = self.data['labels'], self.data['anonlabels'] for name, explicit in document.nametypes.items(): @@ -659,14 +615,6 @@ class StandardDomain(Domain): # type: (str, str, str, str) -> None self.data['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 @@ -705,7 +653,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 @@ -839,6 +790,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: