2 # Author: David Goodger <goodger@python.org>
3 # Copyright: This module has been placed in the public domain.
6 Transforms for resolving references.
9 __docformat__
= 'reStructuredText'
13 from docutils
import nodes
, utils
14 from docutils
.transforms
import TransformError
, Transform
17 class PropagateTargets(Transform
):
20 Propagate empty internal targets to the next element.
22 Given the following nodes::
24 <target ids="internal1" names="internal1">
25 <target anonymous="1" ids="id1">
26 <target ids="internal2" names="internal2">
30 PropagateTargets propagates the ids and names of the internal
31 targets preceding the paragraph to the paragraph itself::
33 <target refid="internal1">
34 <target anonymous="1" refid="id1">
35 <target refid="internal2">
36 <paragraph ids="internal2 id1 internal1" names="internal2 internal1">
40 default_priority
= 260
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'))):
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
85 self
.document
.note_refid(target
)
88 class AnonymousHyperlinks(Transform
):
91 Link anonymous references to targets. Given::
94 <reference anonymous="1">
96 <reference anonymous="1">
98 <target anonymous="1" ids="id1">
99 <target anonymous="1" ids="id2" refuri="http://external">
101 Corresponding references are linked via "refid" or resolved via "refuri"::
104 <reference anonymous="1" refid="id1">
106 <reference anonymous="1" refuri="http://external">
108 <target anonymous="1" ids="id1">
109 <target anonymous="1" ids="id2" refuri="http://external">
112 default_priority
= 440
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
)
137 for ref
, target
in zip(anonymous_refs
, anonymous_targets
):
138 target
.referenced
= 1
140 if target
.hasattr('refuri'):
141 ref
['refuri'] = target
['refuri']
145 if not target
['ids']:
147 target
= self
.document
.ids
[target
['refid']]
149 ref
['refid'] = target
['ids'][0]
150 self
.document
.note_refid(ref
)
154 class IndirectHyperlinks(Transform
):
157 a) Indirect external references::
160 <reference refname="indirect external">
162 <target id="id1" name="direct external"
163 refuri="http://indirect">
164 <target id="id2" name="indirect external"
165 refname="direct external">
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)::
172 <reference refname="indirect external">
174 <target id="id1" name="direct external"
175 refuri="http://indirect">
176 <target id="id2" name="indirect external"
177 refuri="http://indirect">
179 Once the attribute is migrated, the preexisting "refname" attribute
182 b) Indirect internal references::
184 <target id="id1" name="final target">
186 <reference refname="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">
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::
198 <target id="id1" name="final target">
200 <reference refid="id1">
202 <target id="id2" name="indirect internal 2" refid="id1">
203 <target id="id3" name="indirect internal" refid="id1">
206 default_priority
= 460
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
)
214 def resolve_indirect_target(self
, target
):
215 refname
= target
.get('refname')
217 reftarget_id
= target
['refid']
219 reftarget_id
= self
.document
.nameids
.get(refname
)
221 # Check the unknown_reference_resolvers
222 for resolver_function
in \
223 self
.document
.transformer
.unknown_reference_resolvers
:
224 if resolver_function(target
):
227 self
.nonexistent_indirect_target(target
)
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
)
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
:
245 elif reftarget
.hasattr('refid'):
246 target
['refid'] = reftarget
['refid']
247 self
.document
.note_refid(target
)
250 target
['refid'] = reftarget_id
251 self
.document
.note_refid(target
)
253 self
.nonexistent_indirect_target(target
)
255 if refname
is not None:
256 del target
['refname']
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')
264 self
.indirect_target_error(target
, 'which does not exist')
266 def circular_indirect_reference(self
, target
):
267 self
.indirect_target_error(target
, 'forming a circular reference')
269 def indirect_target_error(self
, target
, explanation
):
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, []))
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
)
292 def resolve_indirect_references(self
, target
):
293 if target
.hasattr('refid'):
295 call_method
= self
.document
.note_refid
296 elif target
.hasattr('refuri'):
301 attval
= target
[attname
]
302 for name
in target
['names']:
303 reflist
= self
.document
.refnames
.get(name
, [])
305 target
.note_referenced_by(name
=name
)
310 ref
[attname
] = attval
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, [])
319 target
.note_referenced_by(id=id)
324 ref
[attname
] = attval
328 if isinstance(ref
, nodes
.target
):
329 self
.resolve_indirect_references(ref
)
332 class ExternalTargets(Transform
):
338 <reference refname="direct external">
340 <target id="id1" name="direct external" refuri="http://direct">
342 The "refname" attribute is replaced by the direct "refuri" attribute::
345 <reference refuri="http://direct">
347 <target id="id1" name="direct external" refuri="http://direct">
350 default_priority
= 640
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
, [])
359 target
.note_referenced_by(name
=name
)
364 ref
['refuri'] = refuri
368 class InternalTargets(Transform
):
370 default_priority
= 660
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
)
377 def resolve_reference_ids(self
, target
):
382 <reference refname="direct internal">
384 <target id="id1" name="direct internal">
386 The "refname" attribute is replaced by "refid" linking to the target's
390 <reference refid="id1">
392 <target id="id1" name="direct internal">
394 for name
in target
['names']:
395 refid
= self
.document
.nameids
.get(name
)
396 reflist
= self
.document
.refnames
.get(name
, [])
398 target
.note_referenced_by(name
=name
)
408 class Footnotes(Transform
):
411 Assign numbers to autonumbered footnotes, and resolve links to footnotes,
412 citations, and their references.
414 Given the following ``document`` as input::
418 A labeled autonumbered footnote referece:
419 <footnote_reference auto="1" id="id1" refname="footnote">
421 An unlabeled autonumbered footnote referece:
422 <footnote_reference auto="1" id="id2">
423 <footnote auto="1" id="id3">
425 Unlabeled autonumbered footnote.
426 <footnote auto="1" id="footnote" name="footnote">
428 Labeled autonumbered footnote.
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``.
436 The transformed result will be::
440 A labeled autonumbered footnote referece:
441 <footnote_reference auto="1" id="id1" refid="footnote">
444 An unlabeled autonumbered footnote referece:
445 <footnote_reference auto="1" id="id2" refid="id3">
447 <footnote auto="1" id="id3" backrefs="id2">
451 Unlabeled autonumbered footnote.
452 <footnote auto="1" id="footnote" name="footnote" backrefs="id1">
456 Labeled autonumbered footnote.
458 Note that the footnotes are not in the same order as the references.
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.
465 After adding labels and reference text, the "auto" attributes can be
469 default_priority
= 620
471 autofootnote_labels
= None
472 """Keep track of unlabeled autonumbered footnotes."""
475 # Entries 1-4 and 6 below are from section 12.51 of
476 # The Chicago Manual of Style, 14th edition.
478 u
'\u2020', # dagger †
479 u
'\u2021', # double dagger ‡
480 u
'\u00A7', # section mark §
481 u
'\u00B6', # paragraph mark (pilcrow) ¶
482 # (parallels ['||'] in CMoS)
484 # The entries below were chosen arbitrarily.
485 u
'\u2660', # spade suit ♠
486 u
'\u2665', # heart suit ♥
487 u
'\u2666', # diamond suit ♦
488 u
'\u2663', # club suit ♣
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()
499 def number_footnotes(self
, startnum
):
501 Assign numbers to autonumbered footnotes.
503 For labeled autonumbered footnotes, copy the number over to
504 corresponding footnote references.
506 for footnote
in self
.document
.autofootnotes
:
508 label
= str(startnum
)
510 if label
not in self
.document
.nameids
:
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
)
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
)
528 def number_footnote_references(self
, startnum
):
529 """Assign numbers to autonumbered footnote references."""
531 for ref
in self
.document
.autofootnote_refs
:
532 if ref
.resolved
or ref
.hasattr('refid'):
535 label
= self
.autofootnote_labels
[i
]
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'):
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
)
551 ref
+= nodes
.Text(label
)
552 id = self
.document
.nameids
[label
]
553 footnote
= self
.document
.ids
[id]
555 self
.document
.note_refid(ref
)
556 assert len(ref
['ids']) == 1
557 footnote
.add_backref(ref
['ids'][0])
561 def symbolize_footnotes(self
):
562 """Add symbols indexes to "[*]"-style footnotes and references."""
564 for footnote
in self
.document
.symbol_footnotes
:
565 reps
, index
= divmod(self
.document
.symbol_footnote_start
,
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
)
573 for ref
in self
.document
.symbol_footnote_refs
:
575 ref
+= nodes
.Text(labels
[i
])
577 msg
= self
.document
.reporter
.error(
578 'Too many symbol footnote references: only %s '
579 'corresponding footnotes available.' % len(labels
),
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'):
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
)
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])
598 def resolve_footnotes_and_citations(self
):
600 Link manually-labeled footnotes and citations to/from their
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
)
614 def resolve_references(self
, note
, reflist
):
615 assert len(note
['ids']) == 1
620 ref
.delattr('refname')
622 assert len(ref
['ids']) == 1
623 note
.add_backref(ref
['ids'][0])
628 class CircularSubstitutionDefinitionError(Exception): pass
631 class Substitutions(Transform
):
634 Given the following ``document`` as input::
639 <substitution_reference refname="biohazard">
641 symbol is deservedly scary-looking.
642 <substitution_definition name="biohazard">
643 <image alt="biohazard" uri="biohazard.png">
645 The ``substitution_reference`` will simply be replaced by the
646 contents of the corresponding ``substitution_definition``.
648 The transformed result will be::
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">
659 default_priority
= 220
660 """The Substitutions transform has to be applied very early, before
661 `docutils.tranforms.frontmatter.DocTitle` and others."""
664 defs
= self
.document
.substitution_defs
665 normed
= self
.document
.substitution_names
666 subreflist
= self
.document
.traverse(nodes
.substitution_reference
)
668 for ref
in subreflist
:
669 refname
= ref
['refname']
674 normed_name
= refname
.lower()
675 if normed_name
in normed
:
676 key
= normed
[normed_name
]
678 msg
= self
.document
.reporter
.error(
679 'Undefined substitution referenced: "%s".'
680 % refname
, base_node
=ref
)
681 msgid
= self
.document
.set_id(msg
)
682 prb
= nodes
.problematic(
683 ref
.rawsource
, ref
.rawsource
, refid
=msgid
)
684 prbid
= self
.document
.set_id(prb
)
685 msg
.add_backref(prbid
)
686 ref
.replace_self(prb
)
690 index
= parent
.index(ref
)
691 if ('ltrim' in subdef
.attributes
692 or 'trim' in subdef
.attributes
):
693 if index
> 0 and isinstance(parent
[index
- 1],
695 parent
.replace(parent
[index
- 1],
696 parent
[index
- 1].rstrip())
697 if ('rtrim' in subdef
.attributes
698 or 'trim' in subdef
.attributes
):
699 if (len(parent
) > index
+ 1
700 and isinstance(parent
[index
+ 1], nodes
.Text
)):
701 parent
.replace(parent
[index
+ 1],
702 parent
[index
+ 1].lstrip())
703 subdef_copy
= subdef
.deepcopy()
705 # Take care of nested substitution references:
706 for nested_ref
in subdef_copy
.traverse(
707 nodes
.substitution_reference
):
708 nested_name
= normed
[nested_ref
['refname'].lower()]
709 if nested_name
in nested
.setdefault(nested_name
, []):
710 raise CircularSubstitutionDefinitionError
712 nested
[nested_name
].append(key
)
713 subreflist
.append(nested_ref
)
714 except CircularSubstitutionDefinitionError
:
716 if isinstance(parent
, nodes
.substitution_definition
):
717 msg
= self
.document
.reporter
.error(
718 'Circular substitution definition detected:',
719 nodes
.literal_block(parent
.rawsource
,
721 line
=parent
.line
, base_node
=parent
)
722 parent
.replace_self(msg
)
724 msg
= self
.document
.reporter
.error(
725 'Circular substitution definition referenced: "%s".'
726 % refname
, base_node
=ref
)
727 msgid
= self
.document
.set_id(msg
)
728 prb
= nodes
.problematic(
729 ref
.rawsource
, ref
.rawsource
, refid
=msgid
)
730 prbid
= self
.document
.set_id(prb
)
731 msg
.add_backref(prbid
)
732 ref
.replace_self(prb
)
734 ref
.replace_self(subdef_copy
.children
)
735 # register refname of the replacment node(s)
736 # (needed for resolution of references)
737 for node
in subdef_copy
.children
:
738 if isinstance(node
, nodes
.Referential
):
739 # HACK: verify refname attribute exists.
740 # Test with docs/dev/todo.txt, see. |donate|
741 if 'refname' in node
:
742 self
.document
.note_refname(node
)
745 class TargetNotes(Transform
):
748 Creates a footnote for each external target in the text, and corresponding
749 footnote references after each reference.
752 default_priority
= 540
753 """The TargetNotes transform has to be applied after `IndirectHyperlinks`
754 but before `Footnotes`."""
757 def __init__(self
, document
, startnode
):
758 Transform
.__init
__(self
, document
, startnode
=startnode
)
760 self
.classes
= startnode
.details
.get('class', [])
765 for target
in self
.document
.traverse(nodes
.target
):
766 # Only external targets.
767 if not target
.hasattr('refuri'):
769 names
= target
['names']
772 refs
.extend(self
.document
.refnames
.get(name
, []))
775 footnote
= self
.make_target_footnote(target
['refuri'], refs
,
777 if target
['refuri'] not in notes
:
778 notes
[target
['refuri']] = footnote
779 nodelist
.append(footnote
)
780 # Take care of anonymous references.
781 for ref
in self
.document
.traverse(nodes
.reference
):
782 if not ref
.get('anonymous'):
784 if ref
.hasattr('refuri'):
785 footnote
= self
.make_target_footnote(ref
['refuri'], [ref
],
787 if ref
['refuri'] not in notes
:
788 notes
[ref
['refuri']] = footnote
789 nodelist
.append(footnote
)
790 self
.startnode
.replace_self(nodelist
)
792 def make_target_footnote(self
, refuri
, refs
, notes
):
793 if refuri
in notes
: # duplicate?
794 footnote
= notes
[refuri
]
795 assert len(footnote
['names']) == 1
796 footnote_name
= footnote
['names'][0]
798 footnote
= nodes
.footnote()
799 footnote_id
= self
.document
.set_id(footnote
)
800 # Use uppercase letters and a colon; they can't be
801 # produced inside names by the parser.
802 footnote_name
= 'TARGET_NOTE: ' + footnote_id
804 footnote
['names'] = [footnote_name
]
805 footnote_paragraph
= nodes
.paragraph()
806 footnote_paragraph
+= nodes
.reference('', refuri
, refuri
=refuri
)
807 footnote
+= footnote_paragraph
808 self
.document
.note_autofootnote(footnote
)
809 self
.document
.note_explicit_target(footnote
, footnote
)
811 if isinstance(ref
, nodes
.target
):
813 refnode
= nodes
.footnote_reference(refname
=footnote_name
, auto
=1)
814 refnode
['classes'] += self
.classes
815 self
.document
.note_autofootnote_ref(refnode
)
816 self
.document
.note_footnote_ref(refnode
)
817 index
= ref
.parent
.index(ref
) + 1
819 if not utils
.get_trim_footnote_ref_space(self
.document
.settings
):
821 reflist
.insert(0, nodes
.inline(text
=' ', Classes
=self
.classes
))
823 reflist
.insert(0, nodes
.Text(' '))
824 ref
.parent
.insert(index
, reflist
)
828 class DanglingReferences(Transform
):
831 Check for dangling references (incl. footnote & citation) and for
832 unreferenced targets.
835 default_priority
= 850
838 visitor
= DanglingReferencesVisitor(
840 self
.document
.transformer
.unknown_reference_resolvers
)
841 self
.document
.walk(visitor
)
842 # *After* resolving all references, check for unreferenced
844 for target
in self
.document
.traverse(nodes
.target
):
845 if not target
.referenced
:
846 if target
.get('anonymous'):
847 # If we have unreferenced anonymous targets, there
848 # is already an error message about anonymous
849 # hyperlink mismatch; no need to generate another
853 naming
= target
['names'][0]
855 naming
= target
['ids'][0]
857 # Hack: Propagated targets always have their refid
859 naming
= target
['refid']
860 self
.document
.reporter
.info(
861 'Hyperlink target "%s" is not referenced.'
862 % naming
, base_node
=target
)
865 class DanglingReferencesVisitor(nodes
.SparseNodeVisitor
):
867 def __init__(self
, document
, unknown_reference_resolvers
):
868 nodes
.SparseNodeVisitor
.__init
__(self
, document
)
869 self
.document
= document
870 self
.unknown_reference_resolvers
= unknown_reference_resolvers
872 def unknown_visit(self
, node
):
875 def visit_reference(self
, node
):
876 if node
.resolved
or not node
.hasattr('refname'):
878 refname
= node
['refname']
879 id = self
.document
.nameids
.get(refname
)
881 for resolver_function
in self
.unknown_reference_resolvers
:
882 if resolver_function(node
):
885 if refname
in self
.document
.nameids
:
886 msg
= self
.document
.reporter
.error(
887 'Duplicate target name, cannot be used as a unique '
888 'reference: "%s".' % (node
['refname']), base_node
=node
)
890 msg
= self
.document
.reporter
.error(
891 'Unknown target name: "%s".' % (node
['refname']),
893 msgid
= self
.document
.set_id(msg
)
894 prb
= nodes
.problematic(
895 node
.rawsource
, node
.rawsource
, refid
=msgid
)
896 prbid
= self
.document
.set_id(prb
)
897 msg
.add_backref(prbid
)
898 node
.replace_self(prb
)
902 self
.document
.ids
[id].note_referenced_by(id=id)
905 visit_footnote_reference
= visit_citation_reference
= visit_reference