Mercurial > repos > shellac > guppy_basecaller
comparison env/lib/python3.7/site-packages/docutils/transforms/references.py @ 2:6af9afd405e9 draft
"planemo upload commit 0a63dd5f4d38a1f6944587f52a8cd79874177fc1"
author | shellac |
---|---|
date | Thu, 14 May 2020 14:56:58 -0400 |
parents | 26e78fe6e8c4 |
children |
comparison
equal
deleted
inserted
replaced
1:75ca89e9b81c | 2:6af9afd405e9 |
---|---|
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 † | |
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 | |
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 |