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 † 479 u'\u2021', # double dagger ‡ 480 u'\u00A7', # section mark § 481 u'\u00B6', # paragraph mark (pilcrow) ¶ 482 # (parallels ['||'] in CMoS) 483 '#', # number sign 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 ♣ 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