# $Id: references.py 10211 2025-08-19 20:28:48Z 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 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.
            # -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:
        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 = True

    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 = 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