# $Id: references.py 9950 2024-10-15 11:24:32Z milde $ # Author: David Goodger # Copyright: This module has been placed in the public domain. """ Transforms for resolving references. """ __docformat__ = 'reStructuredText' from docutils import nodes, utils from docutils.transforms import Transform 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. 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: refname = target.get('refname') if refname is None: reftarget_id = target['refid'] else: reftarget_id = self.document.nameids.get(refname) if not reftarget_id: # 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 = self.document.ids[reftarget_id] reftarget.note_referenced_by(id=reftarget_id) 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'] = reftarget_id self.document.note_refid(target) else: self.nonexistent_indirect_target(target) return if refname is not None: del target['refname'] target.resolved = 1 def nonexistent_indirect_target(self, target) -> None: if target['refname'] in self.document.nameids: 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 = 1 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 = 1 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 = 1 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 = 1 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 = 1 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