comparison env/lib/python3.9/site-packages/docutils/transforms/references.py @ 0:4f3585e2f14b draft default tip

"planemo upload commit 60cee0fc7c0cda8592644e1aad72851dec82c959"
author shellac
date Mon, 22 Mar 2021 18:12:50 +0000
parents
children
comparison
equal deleted inserted replaced
-1:000000000000 0:4f3585e2f14b
1 # $Id: references.py 8387 2019-09-06 13:16:34Z milde $
2 # Author: David Goodger <goodger@python.org>
3 # Copyright: This module has been placed in the public domain.
4
5 """
6 Transforms for resolving references.
7 """
8
9 __docformat__ = 'reStructuredText'
10
11 import sys
12 import re
13 from docutils import nodes, utils
14 from docutils.transforms import TransformError, Transform
15
16
17 class 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
88 class 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
154 class 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
332 class 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
368 class 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
408 class 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
628 class CircularSubstitutionDefinitionError(Exception): pass
629
630
631 class 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 subreflist = list(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[index - 1] = parent[index - 1].rstrip()
696 if ('rtrim' in subdef.attributes
697 or 'trim' in subdef.attributes):
698 if (len(parent) > index + 1
699 and isinstance(parent[index + 1], nodes.Text)):
700 parent[index + 1] = parent[index + 1].lstrip()
701 subdef_copy = subdef.deepcopy()
702 try:
703 # Take care of nested substitution references:
704 for nested_ref in subdef_copy.traverse(
705 nodes.substitution_reference):
706 nested_name = normed[nested_ref['refname'].lower()]
707 if nested_name in nested.setdefault(nested_name, []):
708 raise CircularSubstitutionDefinitionError
709 else:
710 nested[nested_name].append(key)
711 nested_ref['ref-origin'] = ref
712 subreflist.append(nested_ref)
713 except CircularSubstitutionDefinitionError:
714 parent = ref.parent
715 if isinstance(parent, nodes.substitution_definition):
716 msg = self.document.reporter.error(
717 'Circular substitution definition detected:',
718 nodes.literal_block(parent.rawsource,
719 parent.rawsource),
720 line=parent.line, base_node=parent)
721 parent.replace_self(msg)
722 else:
723 # find original ref substitution which cased this error
724 ref_origin = ref
725 while ref_origin.hasattr('ref-origin'):
726 ref_origin = ref_origin['ref-origin']
727 msg = self.document.reporter.error(
728 'Circular substitution definition referenced: '
729 '"%s".' % refname, base_node=ref_origin)
730 msgid = self.document.set_id(msg)
731 prb = nodes.problematic(
732 ref.rawsource, ref.rawsource, refid=msgid)
733 prbid = self.document.set_id(prb)
734 msg.add_backref(prbid)
735 ref.replace_self(prb)
736 else:
737 ref.replace_self(subdef_copy.children)
738 # register refname of the replacment node(s)
739 # (needed for resolution of references)
740 for node in subdef_copy.children:
741 if isinstance(node, nodes.Referential):
742 # HACK: verify refname attribute exists.
743 # Test with docs/dev/todo.txt, see. |donate|
744 if 'refname' in node:
745 self.document.note_refname(node)
746
747
748 class TargetNotes(Transform):
749
750 """
751 Creates a footnote for each external target in the text, and corresponding
752 footnote references after each reference.
753 """
754
755 default_priority = 540
756 """The TargetNotes transform has to be applied after `IndirectHyperlinks`
757 but before `Footnotes`."""
758
759
760 def __init__(self, document, startnode):
761 Transform.__init__(self, document, startnode=startnode)
762
763 self.classes = startnode.details.get('class', [])
764
765 def apply(self):
766 notes = {}
767 nodelist = []
768 for target in self.document.traverse(nodes.target):
769 # Only external targets.
770 if not target.hasattr('refuri'):
771 continue
772 names = target['names']
773 refs = []
774 for name in names:
775 refs.extend(self.document.refnames.get(name, []))
776 if not refs:
777 continue
778 footnote = self.make_target_footnote(target['refuri'], refs,
779 notes)
780 if target['refuri'] not in notes:
781 notes[target['refuri']] = footnote
782 nodelist.append(footnote)
783 # Take care of anonymous references.
784 for ref in self.document.traverse(nodes.reference):
785 if not ref.get('anonymous'):
786 continue
787 if ref.hasattr('refuri'):
788 footnote = self.make_target_footnote(ref['refuri'], [ref],
789 notes)
790 if ref['refuri'] not in notes:
791 notes[ref['refuri']] = footnote
792 nodelist.append(footnote)
793 self.startnode.replace_self(nodelist)
794
795 def make_target_footnote(self, refuri, refs, notes):
796 if refuri in notes: # duplicate?
797 footnote = notes[refuri]
798 assert len(footnote['names']) == 1
799 footnote_name = footnote['names'][0]
800 else: # original
801 footnote = nodes.footnote()
802 footnote_id = self.document.set_id(footnote)
803 # Use uppercase letters and a colon; they can't be
804 # produced inside names by the parser.
805 footnote_name = 'TARGET_NOTE: ' + footnote_id
806 footnote['auto'] = 1
807 footnote['names'] = [footnote_name]
808 footnote_paragraph = nodes.paragraph()
809 footnote_paragraph += nodes.reference('', refuri, refuri=refuri)
810 footnote += footnote_paragraph
811 self.document.note_autofootnote(footnote)
812 self.document.note_explicit_target(footnote, footnote)
813 for ref in refs:
814 if isinstance(ref, nodes.target):
815 continue
816 refnode = nodes.footnote_reference(refname=footnote_name, auto=1)
817 refnode['classes'] += self.classes
818 self.document.note_autofootnote_ref(refnode)
819 self.document.note_footnote_ref(refnode)
820 index = ref.parent.index(ref) + 1
821 reflist = [refnode]
822 if not utils.get_trim_footnote_ref_space(self.document.settings):
823 if self.classes:
824 reflist.insert(0, nodes.inline(text=' ', Classes=self.classes))
825 else:
826 reflist.insert(0, nodes.Text(' '))
827 ref.parent.insert(index, reflist)
828 return footnote
829
830
831 class DanglingReferences(Transform):
832
833 """
834 Check for dangling references (incl. footnote & citation) and for
835 unreferenced targets.
836 """
837
838 default_priority = 850
839
840 def apply(self):
841 visitor = DanglingReferencesVisitor(
842 self.document,
843 self.document.transformer.unknown_reference_resolvers)
844 self.document.walk(visitor)
845 # *After* resolving all references, check for unreferenced
846 # targets:
847 for target in self.document.traverse(nodes.target):
848 if not target.referenced:
849 if target.get('anonymous'):
850 # If we have unreferenced anonymous targets, there
851 # is already an error message about anonymous
852 # hyperlink mismatch; no need to generate another
853 # message.
854 continue
855 if target['names']:
856 naming = target['names'][0]
857 elif target['ids']:
858 naming = target['ids'][0]
859 else:
860 # Hack: Propagated targets always have their refid
861 # attribute set.
862 naming = target['refid']
863 self.document.reporter.info(
864 'Hyperlink target "%s" is not referenced.'
865 % naming, base_node=target)
866
867
868 class DanglingReferencesVisitor(nodes.SparseNodeVisitor):
869
870 def __init__(self, document, unknown_reference_resolvers):
871 nodes.SparseNodeVisitor.__init__(self, document)
872 self.document = document
873 self.unknown_reference_resolvers = unknown_reference_resolvers
874
875 def unknown_visit(self, node):
876 pass
877
878 def visit_reference(self, node):
879 if node.resolved or not node.hasattr('refname'):
880 return
881 refname = node['refname']
882 id = self.document.nameids.get(refname)
883 if id is None:
884 for resolver_function in self.unknown_reference_resolvers:
885 if resolver_function(node):
886 break
887 else:
888 if refname in self.document.nameids:
889 msg = self.document.reporter.error(
890 'Duplicate target name, cannot be used as a unique '
891 'reference: "%s".' % (node['refname']), base_node=node)
892 else:
893 msg = self.document.reporter.error(
894 'Unknown target name: "%s".' % (node['refname']),
895 base_node=node)
896 msgid = self.document.set_id(msg)
897 prb = nodes.problematic(
898 node.rawsource, node.rawsource, refid=msgid)
899 try:
900 prbid = node['ids'][0]
901 except IndexError:
902 prbid = self.document.set_id(prb)
903 msg.add_backref(prbid)
904 node.replace_self(prb)
905 else:
906 del node['refname']
907 node['refid'] = id
908 self.document.ids[id].note_referenced_by(id=id)
909 node.resolved = 1
910
911 visit_footnote_reference = visit_citation_reference = visit_reference