# $Id: references.py 10303 2026-03-27 08:41:10Z milde $ # Author: David Goodger # Copyright: This module has been placed in the public domain. """ Transforms for resolving references. """ from __future__ import annotations __docformat__ = 'reStructuredText' from docutils import nodes, utils from docutils.transforms import Transform class SectionIDs(Transform): """ Add identifiers to sections. If the "legacy_ids" configuration setting is False, the rST parser does not generate identifiers for implicit targets (e.g. sections) in order to give explicit targets preferential access to identifiers matching their reference name. However, the `parts.Contents` transform and most writers expect sections to have an identifier, so this transform adds them. """ default_priority = 240 def apply(self) -> None: if getattr(self.document.settings, "legacy_ids", True): return for node in self.document.findall(nodes.section): self.document.set_id(node) class PropagateTargets(Transform): """ Propagate empty internal targets to the next element. Given the following nodes:: This is a test. `PropagateTargets` propagates the ids and names of the internal targets preceding the paragraph to the paragraph itself:: This is a test. """ default_priority = 260 def apply(self) -> None: for target in self.document.findall(nodes.target): # Only block-level targets without reference (like ".. _target:"): if (isinstance(target.parent, nodes.TextElement) or (target.hasattr('refid') or target.hasattr('refuri') or target.hasattr('refname'))): continue assert len(target) == 0, 'error: block-level target has children' next_node = target.next_node(ascend=True) # skip system messages (may be removed by universal.FilterMessages) while isinstance(next_node, nodes.system_message): next_node = next_node.next_node(ascend=True, descend=False) # Do not move names and ids into Invisibles (we'd lose the # attributes) or different Targetables (e.g. footnotes). if (next_node is None or isinstance(next_node, (nodes.Invisible, nodes.Targetable)) and not isinstance(next_node, nodes.target)): continue next_node['ids'].extend(target['ids']) next_node['names'].extend(target['names']) # Set defaults for next_node.expect_referenced_by_name/id. if not hasattr(next_node, 'expect_referenced_by_name'): next_node.expect_referenced_by_name = {} if not hasattr(next_node, 'expect_referenced_by_id'): next_node.expect_referenced_by_id = {} for id in target['ids']: # Update IDs to node mapping. self.document.ids[id] = next_node # If next_node is referenced by id ``id``, this # target shall be marked as referenced. next_node.expect_referenced_by_id[id] = target for name in target['names']: next_node.expect_referenced_by_name[name] = target # If there are any expect_referenced_by_... attributes # in target set, copy them to next_node. next_node.expect_referenced_by_name.update( getattr(target, 'expect_referenced_by_name', {})) next_node.expect_referenced_by_id.update( getattr(target, 'expect_referenced_by_id', {})) # Remove target node from places where it is invalid. # TODO: always remove target? # +1 It did complete its mission and is currently ignored. # (except for the Sphinx LaTeX writer) # -1 It may help a future rST writer. if isinstance(target.parent, nodes.figure) and isinstance( next_node, nodes.caption): target.parent.remove(target) continue # Set refid to point to the first former ID of target # which is now an ID of next_node. target['refid'] = target['ids'][0] # Clear ids and names; they have been moved to next_node. target['ids'] = [] target['names'] = [] self.document.note_refid(target) class AnonymousHyperlinks(Transform): """ Link anonymous references to targets. Given:: internal external Corresponding references are linked via "refid" or resolved via "refuri":: text external """ default_priority = 440 def apply(self) -> None: anonymous_refs = [ node for node in self.document.findall(nodes.reference) if node.get('anonymous')] anonymous_targets = [ node for node in self.document.findall(nodes.target) if node.get('anonymous')] if len(anonymous_refs) != len(anonymous_targets): msg = self.document.reporter.error( 'Anonymous hyperlink mismatch: %s references but %s ' 'targets.\nSee "backrefs" attribute for IDs.' % (len(anonymous_refs), len(anonymous_targets))) msgid = self.document.set_id(msg) for ref in anonymous_refs: prb = nodes.problematic( ref.rawsource, ref.rawsource, refid=msgid) prbid = self.document.set_id(prb) msg.add_backref(prbid) ref.replace_self(prb) return for ref, target in zip(anonymous_refs, anonymous_targets): if ref.hasattr('refid') or ref.hasattr('refuri'): continue target.referenced = True while True: if target.hasattr('refuri'): ref['refuri'] = target['refuri'] ref.resolved = True break else: if not target['ids']: # Propagated target. target = self.document.ids[target['refid']] continue ref['refid'] = target['ids'][0] self.document.note_refid(ref) break class IndirectHyperlinks(Transform): """ a) Indirect external references:: indirect external The "refuri" attribute is migrated back to all indirect targets from the final direct target (i.e. a target not referring to another indirect target):: indirect external Once the attribute is migrated, the preexisting "refname" attribute is dropped. b) Indirect internal references:: indirect internal Targets which indirectly refer to an internal target become one-hop indirect (their "refid" attributes are directly set to the internal target's "id"). References which indirectly refer to an internal target become direct internal references:: indirect internal """ default_priority = 460 def apply(self) -> None: for target in self.document.indirect_targets: if not target.resolved: self.resolve_indirect_target(target) self.resolve_indirect_references(target) def resolve_indirect_target(self, target) -> None: # indirect targets have either a refname or refid attribute refname = target.get('refname') refid = target.get('refid') if refid: reftarget = self.document.ids.get(refid) else: reftarget = self.document.names.get(refname) refid = self.document.nameids.get(refname) if reftarget and not refid: refid = self.document.set_id(reftarget) if not reftarget: # Check the unknown_reference_resolvers for resolver_function in \ self.document.transformer.unknown_reference_resolvers: if resolver_function(target): break else: self.nonexistent_indirect_target(target) return reftarget.note_referenced_by(id=refid) if (isinstance(reftarget, nodes.target) and not reftarget.resolved and reftarget.hasattr('refname')): if hasattr(target, 'multiply_indirect'): self.circular_indirect_reference(target) return target.multiply_indirect = 1 self.resolve_indirect_target(reftarget) # multiply indirect del target.multiply_indirect if reftarget.hasattr('refuri'): target['refuri'] = reftarget['refuri'] if 'refid' in target: del target['refid'] elif reftarget.hasattr('refid'): target['refid'] = reftarget['refid'] self.document.note_refid(target) else: if reftarget['ids']: target['refid'] = refid self.document.note_refid(target) else: self.nonexistent_indirect_target(target) return if refname is not None: del target['refname'] target.resolved = True def nonexistent_indirect_target(self, target) -> None: if self.document.names.get(target['refname'], '') is None: self.indirect_target_error(target, 'which is a duplicate, and ' 'cannot be used as a unique reference') else: self.indirect_target_error(target, 'which does not exist') def circular_indirect_reference(self, target) -> None: self.indirect_target_error(target, 'forming a circular reference') def indirect_target_error(self, target, explanation) -> None: naming = '' reflist = [] if target['names']: naming = '"%s" ' % target['names'][0] for name in target['names']: reflist.extend(self.document.refnames.get(name, [])) for id in target['ids']: reflist.extend(self.document.refids.get(id, [])) if target['ids']: naming += '(id="%s")' % target['ids'][0] msg = self.document.reporter.error( 'Indirect hyperlink target %s refers to target "%s", %s.' % (naming, target['refname'], explanation), base_node=target) msgid = self.document.set_id(msg) for ref in utils.uniq(reflist): prb = nodes.problematic( ref.rawsource, ref.rawsource, refid=msgid) prbid = self.document.set_id(prb) msg.add_backref(prbid) ref.replace_self(prb) target.resolved = True def resolve_indirect_references(self, target) -> None: if target.hasattr('refid'): attname = 'refid' call_method = self.document.note_refid elif target.hasattr('refuri'): attname = 'refuri' call_method = None else: return attval = target[attname] for name in target['names']: reflist = self.document.refnames.get(name, []) if reflist: target.note_referenced_by(name=name) for ref in reflist: if ref.resolved: continue del ref['refname'] ref[attname] = attval if call_method: call_method(ref) ref.resolved = True if isinstance(ref, nodes.target): self.resolve_indirect_references(ref) for id in target['ids']: reflist = self.document.refids.get(id, []) if reflist: target.note_referenced_by(id=id) for ref in reflist: if ref.resolved: continue del ref['refid'] ref[attname] = attval if call_method: call_method(ref) ref.resolved = True if isinstance(ref, nodes.target): self.resolve_indirect_references(ref) class ExternalTargets(Transform): """ Given:: direct external The "refname" attribute is replaced by the direct "refuri" attribute:: direct external """ default_priority = 640 def apply(self) -> None: for target in self.document.findall(nodes.target): if target.hasattr('refuri'): refuri = target['refuri'] for name in target['names']: reflist = self.document.refnames.get(name, []) if reflist: target.note_referenced_by(name=name) for ref in reflist: if ref.resolved: continue del ref['refname'] ref['refuri'] = refuri ref.resolved = True class InternalTargets(Transform): default_priority = 660 def apply(self) -> None: for target in self.document.findall(nodes.target): if not target.hasattr('refuri') and not target.hasattr('refid'): self.resolve_reference_ids(target) def resolve_reference_ids(self, target) -> None: """ Given:: direct internal The "refname" attribute is replaced by "refid" linking to the target's "id":: direct internal """ for name in target['names']: refid = self.document.nameids.get(name) reflist = self.document.refnames.get(name, []) if reflist: target.note_referenced_by(name=name) for ref in reflist: if ref.resolved: continue if refid: del ref['refname'] ref['refid'] = refid ref.resolved = True class Footnotes(Transform): """ Assign numbers to autonumbered footnotes, and resolve links to footnotes, citations, and their references. Given the following ``document`` as input:: A labeled autonumbered footnote reference: An unlabeled autonumbered footnote reference: Unlabeled autonumbered footnote. Labeled autonumbered footnote. Auto-numbered footnotes have attribute ``auto="1"`` and no label. Auto-numbered footnote_references have no reference text (they're empty elements). When resolving the numbering, a ``label`` element is added to the beginning of the ``footnote``, and reference text to the ``footnote_reference``. The transformed result will be:: A labeled autonumbered footnote reference: 2 An unlabeled autonumbered footnote reference: 1