1# $Id: references.py 8565 2020-09-14 10:26:03Z milde $
2# Author: David Goodger <goodger@python.org>
3# Copyright: This module has been placed in the public domain.
4
5"""
6Transforms for resolving references.
7"""
8
9__docformat__ = 'reStructuredText'
10
11import sys
12import re
13from docutils import nodes, utils
14from docutils.transforms import TransformError, Transform
15
16
17class PropagateTargets(Transform):
18
19    """
20    Propagate empty internal targets to the next element.
21
22    Given the following nodes::
23
24        <target ids="internal1" names="internal1">
25        <target anonymous="1" ids="id1">
26        <target ids="internal2" names="internal2">
27        <paragraph>
28            This is a test.
29
30    PropagateTargets propagates the ids and names of the internal
31    targets preceding the paragraph to the paragraph itself::
32
33        <target refid="internal1">
34        <target anonymous="1" refid="id1">
35        <target refid="internal2">
36        <paragraph ids="internal2 id1 internal1" names="internal2 internal1">
37            This is a test.
38    """
39
40    default_priority = 260
41
42    def apply(self):
43        for target in self.document.traverse(nodes.target):
44            # Only block-level targets without reference (like ".. target:"):
45            if (isinstance(target.parent, nodes.TextElement) or
46                (target.hasattr('refid') or target.hasattr('refuri') or
47                 target.hasattr('refname'))):
48                continue
49            assert len(target) == 0, 'error: block-level target has children'
50            next_node = target.next_node(ascend=True)
51            # Do not move names and ids into Invisibles (we'd lose the
52            # attributes) or different Targetables (e.g. footnotes).
53            if (next_node is not None and
54                ((not isinstance(next_node, nodes.Invisible) and
55                  not isinstance(next_node, nodes.Targetable)) or
56                 isinstance(next_node, nodes.target))):
57                next_node['ids'].extend(target['ids'])
58                next_node['names'].extend(target['names'])
59                # Set defaults for next_node.expect_referenced_by_name/id.
60                if not hasattr(next_node, 'expect_referenced_by_name'):
61                    next_node.expect_referenced_by_name = {}
62                if not hasattr(next_node, 'expect_referenced_by_id'):
63                    next_node.expect_referenced_by_id = {}
64                for id in target['ids']:
65                    # Update IDs to node mapping.
66                    self.document.ids[id] = next_node
67                    # If next_node is referenced by id ``id``, this
68                    # target shall be marked as referenced.
69                    next_node.expect_referenced_by_id[id] = target
70                for name in target['names']:
71                    next_node.expect_referenced_by_name[name] = target
72                # If there are any expect_referenced_by_... attributes
73                # in target set, copy them to next_node.
74                next_node.expect_referenced_by_name.update(
75                    getattr(target, 'expect_referenced_by_name', {}))
76                next_node.expect_referenced_by_id.update(
77                    getattr(target, 'expect_referenced_by_id', {}))
78                # Set refid to point to the first former ID of target
79                # which is now an ID of next_node.
80                target['refid'] = target['ids'][0]
81                # Clear ids and names; they have been moved to
82                # next_node.
83                target['ids'] = []
84                target['names'] = []
85                self.document.note_refid(target)
86
87
88class AnonymousHyperlinks(Transform):
89
90    """
91    Link anonymous references to targets.  Given::
92
93        <paragraph>
94            <reference anonymous="1">
95                internal
96            <reference anonymous="1">
97                external
98        <target anonymous="1" ids="id1">
99        <target anonymous="1" ids="id2" refuri="http://external">
100
101    Corresponding references are linked via "refid" or resolved via "refuri"::
102
103        <paragraph>
104            <reference anonymous="1" refid="id1">
105                text
106            <reference anonymous="1" refuri="http://external">
107                external
108        <target anonymous="1" ids="id1">
109        <target anonymous="1" ids="id2" refuri="http://external">
110    """
111
112    default_priority = 440
113
114    def apply(self):
115        anonymous_refs = []
116        anonymous_targets = []
117        for node in self.document.traverse(nodes.reference):
118            if node.get('anonymous'):
119                anonymous_refs.append(node)
120        for node in self.document.traverse(nodes.target):
121            if node.get('anonymous'):
122                anonymous_targets.append(node)
123        if len(anonymous_refs) \
124              != len(anonymous_targets):
125            msg = self.document.reporter.error(
126                  'Anonymous hyperlink mismatch: %s references but %s '
127                  'targets.\nSee "backrefs" attribute for IDs.'
128                  % (len(anonymous_refs), len(anonymous_targets)))
129            msgid = self.document.set_id(msg)
130            for ref in anonymous_refs:
131                prb = nodes.problematic(
132                      ref.rawsource, ref.rawsource, refid=msgid)
133                prbid = self.document.set_id(prb)
134                msg.add_backref(prbid)
135                ref.replace_self(prb)
136            return
137        for ref, target in zip(anonymous_refs, anonymous_targets):
138            target.referenced = 1
139            while True:
140                if target.hasattr('refuri'):
141                    ref['refuri'] = target['refuri']
142                    ref.resolved = 1
143                    break
144                else:
145                    if not target['ids']:
146                        # Propagated target.
147                        target = self.document.ids[target['refid']]
148                        continue
149                    ref['refid'] = target['ids'][0]
150                    self.document.note_refid(ref)
151                    break
152
153
154class IndirectHyperlinks(Transform):
155
156    """
157    a) Indirect external references::
158
159           <paragraph>
160               <reference refname="indirect external">
161                   indirect external
162           <target id="id1" name="direct external"
163               refuri="http://indirect">
164           <target id="id2" name="indirect external"
165               refname="direct external">
166
167       The "refuri" attribute is migrated back to all indirect targets
168       from the final direct target (i.e. a target not referring to
169       another indirect target)::
170
171           <paragraph>
172               <reference refname="indirect external">
173                   indirect external
174           <target id="id1" name="direct external"
175               refuri="http://indirect">
176           <target id="id2" name="indirect external"
177               refuri="http://indirect">
178
179       Once the attribute is migrated, the preexisting "refname" attribute
180       is dropped.
181
182    b) Indirect internal references::
183
184           <target id="id1" name="final target">
185           <paragraph>
186               <reference refname="indirect internal">
187                   indirect internal
188           <target id="id2" name="indirect internal 2"
189               refname="final target">
190           <target id="id3" name="indirect internal"
191               refname="indirect internal 2">
192
193       Targets which indirectly refer to an internal target become one-hop
194       indirect (their "refid" attributes are directly set to the internal
195       target's "id"). References which indirectly refer to an internal
196       target become direct internal references::
197
198           <target id="id1" name="final target">
199           <paragraph>
200               <reference refid="id1">
201                   indirect internal
202           <target id="id2" name="indirect internal 2" refid="id1">
203           <target id="id3" name="indirect internal" refid="id1">
204    """
205
206    default_priority = 460
207
208    def apply(self):
209        for target in self.document.indirect_targets:
210            if not target.resolved:
211                self.resolve_indirect_target(target)
212            self.resolve_indirect_references(target)
213
214    def resolve_indirect_target(self, target):
215        refname = target.get('refname')
216        if refname is None:
217            reftarget_id = target['refid']
218        else:
219            reftarget_id = self.document.nameids.get(refname)
220            if not reftarget_id:
221                # Check the unknown_reference_resolvers
222                for resolver_function in \
223                        self.document.transformer.unknown_reference_resolvers:
224                    if resolver_function(target):
225                        break
226                else:
227                    self.nonexistent_indirect_target(target)
228                return
229        reftarget = self.document.ids[reftarget_id]
230        reftarget.note_referenced_by(id=reftarget_id)
231        if isinstance(reftarget, nodes.target) \
232               and not reftarget.resolved and reftarget.hasattr('refname'):
233            if hasattr(target, 'multiply_indirect'):
234                #and target.multiply_indirect):
235                #del target.multiply_indirect
236                self.circular_indirect_reference(target)
237                return
238            target.multiply_indirect = 1
239            self.resolve_indirect_target(reftarget) # multiply indirect
240            del target.multiply_indirect
241        if reftarget.hasattr('refuri'):
242            target['refuri'] = reftarget['refuri']
243            if 'refid' in target:
244                del target['refid']
245        elif reftarget.hasattr('refid'):
246            target['refid'] = reftarget['refid']
247            self.document.note_refid(target)
248        else:
249            if reftarget['ids']:
250                target['refid'] = reftarget_id
251                self.document.note_refid(target)
252            else:
253                self.nonexistent_indirect_target(target)
254                return
255        if refname is not None:
256            del target['refname']
257        target.resolved = 1
258
259    def nonexistent_indirect_target(self, target):
260        if target['refname'] in self.document.nameids:
261            self.indirect_target_error(target, 'which is a duplicate, and '
262                                       'cannot be used as a unique reference')
263        else:
264            self.indirect_target_error(target, 'which does not exist')
265
266    def circular_indirect_reference(self, target):
267        self.indirect_target_error(target, 'forming a circular reference')
268
269    def indirect_target_error(self, target, explanation):
270        naming = ''
271        reflist = []
272        if target['names']:
273            naming = '"%s" ' % target['names'][0]
274        for name in target['names']:
275            reflist.extend(self.document.refnames.get(name, []))
276        for id in target['ids']:
277            reflist.extend(self.document.refids.get(id, []))
278        if target['ids']:
279            naming += '(id="%s")' % target['ids'][0]
280        msg = self.document.reporter.error(
281              'Indirect hyperlink target %s refers to target "%s", %s.'
282              % (naming, target['refname'], explanation), base_node=target)
283        msgid = self.document.set_id(msg)
284        for ref in utils.uniq(reflist):
285            prb = nodes.problematic(
286                  ref.rawsource, ref.rawsource, refid=msgid)
287            prbid = self.document.set_id(prb)
288            msg.add_backref(prbid)
289            ref.replace_self(prb)
290        target.resolved = 1
291
292    def resolve_indirect_references(self, target):
293        if target.hasattr('refid'):
294            attname = 'refid'
295            call_method = self.document.note_refid
296        elif target.hasattr('refuri'):
297            attname = 'refuri'
298            call_method = None
299        else:
300            return
301        attval = target[attname]
302        for name in target['names']:
303            reflist = self.document.refnames.get(name, [])
304            if reflist:
305                target.note_referenced_by(name=name)
306            for ref in reflist:
307                if ref.resolved:
308                    continue
309                del ref['refname']
310                ref[attname] = attval
311                if call_method:
312                    call_method(ref)
313                ref.resolved = 1
314                if isinstance(ref, nodes.target):
315                    self.resolve_indirect_references(ref)
316        for id in target['ids']:
317            reflist = self.document.refids.get(id, [])
318            if reflist:
319                target.note_referenced_by(id=id)
320            for ref in reflist:
321                if ref.resolved:
322                    continue
323                del ref['refid']
324                ref[attname] = attval
325                if call_method:
326                    call_method(ref)
327                ref.resolved = 1
328                if isinstance(ref, nodes.target):
329                    self.resolve_indirect_references(ref)
330
331
332class ExternalTargets(Transform):
333
334    """
335    Given::
336
337        <paragraph>
338            <reference refname="direct external">
339                direct external
340        <target id="id1" name="direct external" refuri="http://direct">
341
342    The "refname" attribute is replaced by the direct "refuri" attribute::
343
344        <paragraph>
345            <reference refuri="http://direct">
346                direct external
347        <target id="id1" name="direct external" refuri="http://direct">
348    """
349
350    default_priority = 640
351
352    def apply(self):
353        for target in self.document.traverse(nodes.target):
354            if target.hasattr('refuri'):
355                refuri = target['refuri']
356                for name in target['names']:
357                    reflist = self.document.refnames.get(name, [])
358                    if reflist:
359                        target.note_referenced_by(name=name)
360                    for ref in reflist:
361                        if ref.resolved:
362                            continue
363                        del ref['refname']
364                        ref['refuri'] = refuri
365                        ref.resolved = 1
366
367
368class InternalTargets(Transform):
369
370    default_priority = 660
371
372    def apply(self):
373        for target in self.document.traverse(nodes.target):
374            if not target.hasattr('refuri') and not target.hasattr('refid'):
375                self.resolve_reference_ids(target)
376
377    def resolve_reference_ids(self, target):
378        """
379        Given::
380
381            <paragraph>
382                <reference refname="direct internal">
383                    direct internal
384            <target id="id1" name="direct internal">
385
386        The "refname" attribute is replaced by "refid" linking to the target's
387        "id"::
388
389            <paragraph>
390                <reference refid="id1">
391                    direct internal
392            <target id="id1" name="direct internal">
393        """
394        for name in target['names']:
395            refid = self.document.nameids.get(name)
396            reflist = self.document.refnames.get(name, [])
397            if reflist:
398                target.note_referenced_by(name=name)
399            for ref in reflist:
400                if ref.resolved:
401                    continue
402                if refid:
403                    del ref['refname']
404                    ref['refid'] = refid
405                ref.resolved = 1
406
407
408class Footnotes(Transform):
409
410    """
411    Assign numbers to autonumbered footnotes, and resolve links to footnotes,
412    citations, and their references.
413
414    Given the following ``document`` as input::
415
416        <document>
417            <paragraph>
418                A labeled autonumbered footnote referece:
419                <footnote_reference auto="1" id="id1" refname="footnote">
420            <paragraph>
421                An unlabeled autonumbered footnote referece:
422                <footnote_reference auto="1" id="id2">
423            <footnote auto="1" id="id3">
424                <paragraph>
425                    Unlabeled autonumbered footnote.
426            <footnote auto="1" id="footnote" name="footnote">
427                <paragraph>
428                    Labeled autonumbered footnote.
429
430    Auto-numbered footnotes have attribute ``auto="1"`` and no label.
431    Auto-numbered footnote_references have no reference text (they're
432    empty elements). When resolving the numbering, a ``label`` element
433    is added to the beginning of the ``footnote``, and reference text
434    to the ``footnote_reference``.
435
436    The transformed result will be::
437
438        <document>
439            <paragraph>
440                A labeled autonumbered footnote referece:
441                <footnote_reference auto="1" id="id1" refid="footnote">
442                    2
443            <paragraph>
444                An unlabeled autonumbered footnote referece:
445                <footnote_reference auto="1" id="id2" refid="id3">
446                    1
447            <footnote auto="1" id="id3" backrefs="id2">
448                <label>
449                    1
450                <paragraph>
451                    Unlabeled autonumbered footnote.
452            <footnote auto="1" id="footnote" name="footnote" backrefs="id1">
453                <label>
454                    2
455                <paragraph>
456                    Labeled autonumbered footnote.
457
458    Note that the footnotes are not in the same order as the references.
459
460    The labels and reference text are added to the auto-numbered ``footnote``
461    and ``footnote_reference`` elements.  Footnote elements are backlinked to
462    their references via "refids" attributes.  References are assigned "id"
463    and "refid" attributes.
464
465    After adding labels and reference text, the "auto" attributes can be
466    ignored.
467    """
468
469    default_priority = 620
470
471    autofootnote_labels = None
472    """Keep track of unlabeled autonumbered footnotes."""
473
474    symbols = [
475          # Entries 1-4 and 6 below are from section 12.51 of
476          # The Chicago Manual of Style, 14th edition.
477          '*',                          # asterisk/star
478          u'\u2020',                    # dagger &dagger;
479          u'\u2021',                    # double dagger &Dagger;
480          u'\u00A7',                    # section mark &sect;
481          u'\u00B6',                    # paragraph mark (pilcrow) &para;
482                                        # (parallels ['||'] in CMoS)
483          '#',                          # number sign
484          # The entries below were chosen arbitrarily.
485          u'\u2660',                    # spade suit &spades;
486          u'\u2665',                    # heart suit &hearts;
487          u'\u2666',                    # diamond suit &diams;
488          u'\u2663',                    # club suit &clubs;
489          ]
490
491    def apply(self):
492        self.autofootnote_labels = []
493        startnum = self.document.autofootnote_start
494        self.document.autofootnote_start = self.number_footnotes(startnum)
495        self.number_footnote_references(startnum)
496        self.symbolize_footnotes()
497        self.resolve_footnotes_and_citations()
498
499    def number_footnotes(self, startnum):
500        """
501        Assign numbers to autonumbered footnotes.
502
503        For labeled autonumbered footnotes, copy the number over to
504        corresponding footnote references.
505        """
506        for footnote in self.document.autofootnotes:
507            while True:
508                label = str(startnum)
509                startnum += 1
510                if label not in self.document.nameids:
511                    break
512            footnote.insert(0, nodes.label('', label))
513            for name in footnote['names']:
514                for ref in self.document.footnote_refs.get(name, []):
515                    ref += nodes.Text(label)
516                    ref.delattr('refname')
517                    assert len(footnote['ids']) == len(ref['ids']) == 1
518                    ref['refid'] = footnote['ids'][0]
519                    footnote.add_backref(ref['ids'][0])
520                    self.document.note_refid(ref)
521                    ref.resolved = 1
522            if not footnote['names'] and not footnote['dupnames']:
523                footnote['names'].append(label)
524                self.document.note_explicit_target(footnote, footnote)
525                self.autofootnote_labels.append(label)
526        return startnum
527
528    def number_footnote_references(self, startnum):
529        """Assign numbers to autonumbered footnote references."""
530        i = 0
531        for ref in self.document.autofootnote_refs:
532            if ref.resolved or ref.hasattr('refid'):
533                continue
534            try:
535                label = self.autofootnote_labels[i]
536            except IndexError:
537                msg = self.document.reporter.error(
538                      'Too many autonumbered footnote references: only %s '
539                      'corresponding footnotes available.'
540                      % len(self.autofootnote_labels), base_node=ref)
541                msgid = self.document.set_id(msg)
542                for ref in self.document.autofootnote_refs[i:]:
543                    if ref.resolved or ref.hasattr('refname'):
544                        continue
545                    prb = nodes.problematic(
546                          ref.rawsource, ref.rawsource, refid=msgid)
547                    prbid = self.document.set_id(prb)
548                    msg.add_backref(prbid)
549                    ref.replace_self(prb)
550                break
551            ref += nodes.Text(label)
552            id = self.document.nameids[label]
553            footnote = self.document.ids[id]
554            ref['refid'] = id
555            self.document.note_refid(ref)
556            assert len(ref['ids']) == 1
557            footnote.add_backref(ref['ids'][0])
558            ref.resolved = 1
559            i += 1
560
561    def symbolize_footnotes(self):
562        """Add symbols indexes to "[*]"-style footnotes and references."""
563        labels = []
564        for footnote in self.document.symbol_footnotes:
565            reps, index = divmod(self.document.symbol_footnote_start,
566                                 len(self.symbols))
567            labeltext = self.symbols[index] * (reps + 1)
568            labels.append(labeltext)
569            footnote.insert(0, nodes.label('', labeltext))
570            self.document.symbol_footnote_start += 1
571            self.document.set_id(footnote)
572        i = 0
573        for ref in self.document.symbol_footnote_refs:
574            try:
575                ref += nodes.Text(labels[i])
576            except IndexError:
577                msg = self.document.reporter.error(
578                      'Too many symbol footnote references: only %s '
579                      'corresponding footnotes available.' % len(labels),
580                      base_node=ref)
581                msgid = self.document.set_id(msg)
582                for ref in self.document.symbol_footnote_refs[i:]:
583                    if ref.resolved or ref.hasattr('refid'):
584                        continue
585                    prb = nodes.problematic(
586                          ref.rawsource, ref.rawsource, refid=msgid)
587                    prbid = self.document.set_id(prb)
588                    msg.add_backref(prbid)
589                    ref.replace_self(prb)
590                break
591            footnote = self.document.symbol_footnotes[i]
592            assert len(footnote['ids']) == 1
593            ref['refid'] = footnote['ids'][0]
594            self.document.note_refid(ref)
595            footnote.add_backref(ref['ids'][0])
596            i += 1
597
598    def resolve_footnotes_and_citations(self):
599        """
600        Link manually-labeled footnotes and citations to/from their
601        references.
602        """
603        for footnote in self.document.footnotes:
604            for label in footnote['names']:
605                if label in self.document.footnote_refs:
606                    reflist = self.document.footnote_refs[label]
607                    self.resolve_references(footnote, reflist)
608        for citation in self.document.citations:
609            for label in citation['names']:
610                if label in self.document.citation_refs:
611                    reflist = self.document.citation_refs[label]
612                    self.resolve_references(citation, reflist)
613
614    def resolve_references(self, note, reflist):
615        assert len(note['ids']) == 1
616        id = note['ids'][0]
617        for ref in reflist:
618            if ref.resolved:
619                continue
620            ref.delattr('refname')
621            ref['refid'] = id
622            assert len(ref['ids']) == 1
623            note.add_backref(ref['ids'][0])
624            ref.resolved = 1
625        note.resolved = 1
626
627
628class CircularSubstitutionDefinitionError(Exception): pass
629
630
631class Substitutions(Transform):
632
633    """
634    Given the following ``document`` as input::
635
636        <document>
637            <paragraph>
638                The
639                <substitution_reference refname="biohazard">
640                    biohazard
641                 symbol is deservedly scary-looking.
642            <substitution_definition name="biohazard">
643                <image alt="biohazard" uri="biohazard.png">
644
645    The ``substitution_reference`` will simply be replaced by the
646    contents of the corresponding ``substitution_definition``.
647
648    The transformed result will be::
649
650        <document>
651            <paragraph>
652                The
653                <image alt="biohazard" uri="biohazard.png">
654                 symbol is deservedly scary-looking.
655            <substitution_definition name="biohazard">
656                <image alt="biohazard" uri="biohazard.png">
657    """
658
659    default_priority = 220
660    """The Substitutions transform has to be applied very early, before
661    `docutils.tranforms.frontmatter.DocTitle` and others."""
662
663    def apply(self):
664        defs = self.document.substitution_defs
665        normed = self.document.substitution_names
666        nested = {}
667        line_length_limit = getattr(self.document.settings,
668                                    "line_length_limit", 10000)
669
670        subreflist = list(self.document.traverse(nodes.substitution_reference))
671        for ref in subreflist:
672            msg = ''
673            refname = ref['refname']
674            if refname in defs:
675                key = refname
676            else:
677                normed_name = refname.lower()
678                key = normed.get(normed_name, None)
679            if key is None:
680                msg = self.document.reporter.error(
681                      'Undefined substitution referenced: "%s".'
682                      % refname, base_node=ref)
683            else:
684                subdef = defs[key]
685                if len(subdef.astext()) > line_length_limit:
686                    msg = self.document.reporter.error(
687                            'Substitution definition "%s" exceeds the'
688                            ' line-length-limit.' % (key))
689            if msg:
690                msgid = self.document.set_id(msg)
691                prb = nodes.problematic(
692                      ref.rawsource, ref.rawsource, refid=msgid)
693                prbid = self.document.set_id(prb)
694                msg.add_backref(prbid)
695                ref.replace_self(prb)
696                continue
697
698            parent = ref.parent
699            index = parent.index(ref)
700            if  ('ltrim' in subdef.attributes
701                    or 'trim' in subdef.attributes):
702                if index > 0 and isinstance(parent[index - 1],
703                                            nodes.Text):
704                    parent[index - 1] = parent[index - 1].rstrip()
705            if  ('rtrim' in subdef.attributes
706                    or 'trim' in subdef.attributes):
707                if  (len(parent) > index + 1
708                        and isinstance(parent[index + 1], nodes.Text)):
709                    parent[index + 1] = parent[index + 1].lstrip()
710            subdef_copy = subdef.deepcopy()
711            try:
712                # Take care of nested substitution references:
713                for nested_ref in subdef_copy.traverse(
714                        nodes.substitution_reference):
715                    nested_name = normed[nested_ref['refname'].lower()]
716                    if nested_name in nested.setdefault(nested_name, []):
717                        raise CircularSubstitutionDefinitionError
718                    nested[nested_name].append(key)
719                    nested_ref['ref-origin'] = ref
720                    subreflist.append(nested_ref)
721            except CircularSubstitutionDefinitionError:
722                parent = ref.parent
723                if isinstance(parent, nodes.substitution_definition):
724                    msg = self.document.reporter.error(
725                        'Circular substitution definition detected:',
726                        nodes.literal_block(parent.rawsource,
727                                            parent.rawsource),
728                        line=parent.line, base_node=parent)
729                    parent.replace_self(msg)
730                else:
731                    # find original ref substitution which caused this error
732                    ref_origin = ref
733                    while ref_origin.hasattr('ref-origin'):
734                        ref_origin = ref_origin['ref-origin']
735                    msg = self.document.reporter.error(
736                        'Circular substitution definition referenced: '
737                        '"%s".' % refname, base_node=ref_origin)
738                    msgid = self.document.set_id(msg)
739                    prb = nodes.problematic(
740                        ref.rawsource, ref.rawsource, refid=msgid)
741                    prbid = self.document.set_id(prb)
742                    msg.add_backref(prbid)
743                    ref.replace_self(prb)
744                continue
745            ref.replace_self(subdef_copy.children)
746            # register refname of the replacment node(s)
747            # (needed for resolution of references)
748            for node in subdef_copy.children:
749                if isinstance(node, nodes.Referential):
750                    # HACK: verify refname attribute exists.
751                    # Test with docs/dev/todo.txt, see. |donate|
752                    if 'refname' in node:
753                        self.document.note_refname(node)
754
755
756class TargetNotes(Transform):
757
758    """
759    Creates a footnote for each external target in the text, and corresponding
760    footnote references after each reference.
761    """
762
763    default_priority = 540
764    """The TargetNotes transform has to be applied after `IndirectHyperlinks`
765    but before `Footnotes`."""
766
767
768    def __init__(self, document, startnode):
769        Transform.__init__(self, document, startnode=startnode)
770
771        self.classes = startnode.details.get('class', [])
772
773    def apply(self):
774        notes = {}
775        nodelist = []
776        for target in self.document.traverse(nodes.target):
777            # Only external targets.
778            if not target.hasattr('refuri'):
779                continue
780            names = target['names']
781            refs = []
782            for name in names:
783                refs.extend(self.document.refnames.get(name, []))
784            if not refs:
785                continue
786            footnote = self.make_target_footnote(target['refuri'], refs,
787                                                 notes)
788            if target['refuri'] not in notes:
789                notes[target['refuri']] = footnote
790                nodelist.append(footnote)
791        # Take care of anonymous references.
792        for ref in self.document.traverse(nodes.reference):
793            if not ref.get('anonymous'):
794                continue
795            if ref.hasattr('refuri'):
796                footnote = self.make_target_footnote(ref['refuri'], [ref],
797                                                     notes)
798                if ref['refuri'] not in notes:
799                    notes[ref['refuri']] = footnote
800                    nodelist.append(footnote)
801        self.startnode.replace_self(nodelist)
802
803    def make_target_footnote(self, refuri, refs, notes):
804        if refuri in notes:  # duplicate?
805            footnote = notes[refuri]
806            assert len(footnote['names']) == 1
807            footnote_name = footnote['names'][0]
808        else:                           # original
809            footnote = nodes.footnote()
810            footnote_id = self.document.set_id(footnote)
811            # Use uppercase letters and a colon; they can't be
812            # produced inside names by the parser.
813            footnote_name = 'TARGET_NOTE: ' + footnote_id
814            footnote['auto'] = 1
815            footnote['names'] = [footnote_name]
816            footnote_paragraph = nodes.paragraph()
817            footnote_paragraph += nodes.reference('', refuri, refuri=refuri)
818            footnote += footnote_paragraph
819            self.document.note_autofootnote(footnote)
820            self.document.note_explicit_target(footnote, footnote)
821        for ref in refs:
822            if isinstance(ref, nodes.target):
823                continue
824            refnode = nodes.footnote_reference(refname=footnote_name, auto=1)
825            refnode['classes'] += self.classes
826            self.document.note_autofootnote_ref(refnode)
827            self.document.note_footnote_ref(refnode)
828            index = ref.parent.index(ref) + 1
829            reflist = [refnode]
830            if not utils.get_trim_footnote_ref_space(self.document.settings):
831                if self.classes:
832                    reflist.insert(0, nodes.inline(text=' ', Classes=self.classes))
833                else:
834                    reflist.insert(0, nodes.Text(' '))
835            ref.parent.insert(index, reflist)
836        return footnote
837
838
839class DanglingReferences(Transform):
840
841    """
842    Check for dangling references (incl. footnote & citation) and for
843    unreferenced targets.
844    """
845
846    default_priority = 850
847
848    def apply(self):
849        visitor = DanglingReferencesVisitor(
850            self.document,
851            self.document.transformer.unknown_reference_resolvers)
852        self.document.walk(visitor)
853        # *After* resolving all references, check for unreferenced
854        # targets:
855        for target in self.document.traverse(nodes.target):
856            if not target.referenced:
857                if target.get('anonymous'):
858                    # If we have unreferenced anonymous targets, there
859                    # is already an error message about anonymous
860                    # hyperlink mismatch; no need to generate another
861                    # message.
862                    continue
863                if target['names']:
864                    naming = target['names'][0]
865                elif target['ids']:
866                    naming = target['ids'][0]
867                else:
868                    # Hack: Propagated targets always have their refid
869                    # attribute set.
870                    naming = target['refid']
871                self.document.reporter.info(
872                    'Hyperlink target "%s" is not referenced.'
873                    % naming, base_node=target)
874
875
876class DanglingReferencesVisitor(nodes.SparseNodeVisitor):
877
878    def __init__(self, document, unknown_reference_resolvers):
879        nodes.SparseNodeVisitor.__init__(self, document)
880        self.document = document
881        self.unknown_reference_resolvers = unknown_reference_resolvers
882
883    def unknown_visit(self, node):
884        pass
885
886    def visit_reference(self, node):
887        if node.resolved or not node.hasattr('refname'):
888            return
889        refname = node['refname']
890        id = self.document.nameids.get(refname)
891        if id is None:
892            for resolver_function in self.unknown_reference_resolvers:
893                if resolver_function(node):
894                    break
895            else:
896                if refname in self.document.nameids:
897                    msg = self.document.reporter.error(
898                        'Duplicate target name, cannot be used as a unique '
899                        'reference: "%s".' % (node['refname']), base_node=node)
900                else:
901                    msg = self.document.reporter.error(
902                        'Unknown target name: "%s".' % (node['refname']),
903                        base_node=node)
904                msgid = self.document.set_id(msg)
905                prb = nodes.problematic(
906                      node.rawsource, node.rawsource, refid=msgid)
907                try:
908                    prbid = node['ids'][0]
909                except IndexError:
910                    prbid = self.document.set_id(prb)
911                msg.add_backref(prbid)
912                node.replace_self(prb)
913        else:
914            del node['refname']
915            node['refid'] = id
916            self.document.ids[id].note_referenced_by(id=id)
917            node.resolved = 1
918
919    visit_footnote_reference = visit_citation_reference = visit_reference
920