Fix [ 3607029 ] traceback with embedded alias pointing to missing target.
[docutils.git] / docutils / docutils / transforms / references.py
blob25c3e72f2fb80420e0b5785487ca82920713b616
1 # $Id$
2 # Author: David Goodger <goodger@python.org>
3 # Copyright: This module has been placed in the public domain.
5 """
6 Transforms for resolving references.
7 """
9 __docformat__ = 'reStructuredText'
11 import sys
12 import re
13 from docutils import nodes, utils
14 from docutils.transforms import TransformError, Transform
17 class PropagateTargets(Transform):
19 """
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">
27 <paragraph>
28 This is a test.
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">
37 This is a test.
38 """
40 default_priority = 260
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)
88 class AnonymousHyperlinks(Transform):
90 """
91 Link anonymous references to targets. Given::
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">
101 Corresponding references are linked via "refid" or resolved via "refuri"::
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">
112 default_priority = 440
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
154 class IndirectHyperlinks(Transform):
157 a) Indirect external references::
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">
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)::
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">
179 Once the attribute is migrated, the preexisting "refname" attribute
180 is dropped.
182 b) Indirect internal references::
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">
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">
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">
206 default_priority = 460
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)
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
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')
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):
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
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)
332 class ExternalTargets(Transform):
335 Given::
337 <paragraph>
338 <reference refname="direct external">
339 direct external
340 <target id="id1" name="direct external" refuri="http://direct">
342 The "refname" attribute is replaced by the direct "refuri" attribute::
344 <paragraph>
345 <reference refuri="http://direct">
346 direct external
347 <target id="id1" name="direct external" refuri="http://direct">
350 default_priority = 640
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
368 class InternalTargets(Transform):
370 default_priority = 660
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)
377 def resolve_reference_ids(self, target):
379 Given::
381 <paragraph>
382 <reference refname="direct internal">
383 direct internal
384 <target id="id1" name="direct internal">
386 The "refname" attribute is replaced by "refid" linking to the target's
387 "id"::
389 <paragraph>
390 <reference refid="id1">
391 direct internal
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, [])
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
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::
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.
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::
438 <document>
439 <paragraph>
440 A labeled autonumbered footnote referece:
441 <footnote_reference auto="1" id="id1" refid="footnote">
443 <paragraph>
444 An unlabeled autonumbered footnote referece:
445 <footnote_reference auto="1" id="id2" refid="id3">
447 <footnote auto="1" id="id3" backrefs="id2">
448 <label>
450 <paragraph>
451 Unlabeled autonumbered footnote.
452 <footnote auto="1" id="footnote" name="footnote" backrefs="id1">
453 <label>
455 <paragraph>
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
466 ignored.
469 default_priority = 620
471 autofootnote_labels = None
472 """Keep track of unlabeled autonumbered footnotes."""
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;
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()
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:
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
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
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
598 def resolve_footnotes_and_citations(self):
600 Link manually-labeled footnotes and citations to/from their
601 references.
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
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
628 class CircularSubstitutionDefinitionError(Exception): pass
631 class Substitutions(Transform):
634 Given the following ``document`` as input::
636 <document>
637 <paragraph>
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">
645 The ``substitution_reference`` will simply be replaced by the
646 contents of the corresponding ``substitution_definition``.
648 The transformed result will be::
650 <document>
651 <paragraph>
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."""
663 def apply(self):
664 defs = self.document.substitution_defs
665 normed = self.document.substitution_names
666 subreflist = self.document.traverse(nodes.substitution_reference)
667 nested = {}
668 for ref in subreflist:
669 refname = ref['refname']
670 key = None
671 if refname in defs:
672 key = refname
673 else:
674 normed_name = refname.lower()
675 if normed_name in normed:
676 key = normed[normed_name]
677 if key is None:
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)
687 else:
688 subdef = defs[key]
689 parent = ref.parent
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],
694 nodes.Text):
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()
704 try:
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
711 else:
712 nested[nested_name].append(key)
713 subreflist.append(nested_ref)
714 except CircularSubstitutionDefinitionError:
715 parent = ref.parent
716 if isinstance(parent, nodes.substitution_definition):
717 msg = self.document.reporter.error(
718 'Circular substitution definition detected:',
719 nodes.literal_block(parent.rawsource,
720 parent.rawsource),
721 line=parent.line, base_node=parent)
722 parent.replace_self(msg)
723 else:
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)
733 else:
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', [])
762 def apply(self):
763 notes = {}
764 nodelist = []
765 for target in self.document.traverse(nodes.target):
766 # Only external targets.
767 if not target.hasattr('refuri'):
768 continue
769 names = target['names']
770 refs = []
771 for name in names:
772 refs.extend(self.document.refnames.get(name, []))
773 if not refs:
774 continue
775 footnote = self.make_target_footnote(target['refuri'], refs,
776 notes)
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'):
783 continue
784 if ref.hasattr('refuri'):
785 footnote = self.make_target_footnote(ref['refuri'], [ref],
786 notes)
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]
797 else: # original
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
803 footnote['auto'] = 1
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)
810 for ref in refs:
811 if isinstance(ref, nodes.target):
812 continue
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
818 reflist = [refnode]
819 if not utils.get_trim_footnote_ref_space(self.document.settings):
820 if self.classes:
821 reflist.insert(0, nodes.inline(text=' ', Classes=self.classes))
822 else:
823 reflist.insert(0, nodes.Text(' '))
824 ref.parent.insert(index, reflist)
825 return footnote
828 class DanglingReferences(Transform):
831 Check for dangling references (incl. footnote & citation) and for
832 unreferenced targets.
835 default_priority = 850
837 def apply(self):
838 visitor = DanglingReferencesVisitor(
839 self.document,
840 self.document.transformer.unknown_reference_resolvers)
841 self.document.walk(visitor)
842 # *After* resolving all references, check for unreferenced
843 # targets:
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
850 # message.
851 continue
852 if target['names']:
853 naming = target['names'][0]
854 elif target['ids']:
855 naming = target['ids'][0]
856 else:
857 # Hack: Propagated targets always have their refid
858 # attribute set.
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):
873 pass
875 def visit_reference(self, node):
876 if node.resolved or not node.hasattr('refname'):
877 return
878 refname = node['refname']
879 id = self.document.nameids.get(refname)
880 if id is None:
881 for resolver_function in self.unknown_reference_resolvers:
882 if resolver_function(node):
883 break
884 else:
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)
889 else:
890 msg = self.document.reporter.error(
891 'Unknown target name: "%s".' % (node['refname']),
892 base_node=node)
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)
899 else:
900 del node['refname']
901 node['refid'] = id
902 self.document.ids[id].note_referenced_by(id=id)
903 node.resolved = 1
905 visit_footnote_reference = visit_citation_reference = visit_reference