1 # $Id: references.py 5018 2007-03-12 21:39:31Z wiemann $ |
|
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=1) |
|
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 1: |
|
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 target.has_key('refid'): |
|
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 self.document.nameids.has_key(target['refname']): |
|
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 naming += '(id="%s")' % target['ids'][0] |
|
279 msg = self.document.reporter.error( |
|
280 'Indirect hyperlink target %s refers to target "%s", %s.' |
|
281 % (naming, target['refname'], explanation), base_node=target) |
|
282 msgid = self.document.set_id(msg) |
|
283 for ref in utils.uniq(reflist): |
|
284 prb = nodes.problematic( |
|
285 ref.rawsource, ref.rawsource, refid=msgid) |
|
286 prbid = self.document.set_id(prb) |
|
287 msg.add_backref(prbid) |
|
288 ref.replace_self(prb) |
|
289 target.resolved = 1 |
|
290 |
|
291 def resolve_indirect_references(self, target): |
|
292 if target.hasattr('refid'): |
|
293 attname = 'refid' |
|
294 call_method = self.document.note_refid |
|
295 elif target.hasattr('refuri'): |
|
296 attname = 'refuri' |
|
297 call_method = None |
|
298 else: |
|
299 return |
|
300 attval = target[attname] |
|
301 for name in target['names']: |
|
302 reflist = self.document.refnames.get(name, []) |
|
303 if reflist: |
|
304 target.note_referenced_by(name=name) |
|
305 for ref in reflist: |
|
306 if ref.resolved: |
|
307 continue |
|
308 del ref['refname'] |
|
309 ref[attname] = attval |
|
310 if call_method: |
|
311 call_method(ref) |
|
312 ref.resolved = 1 |
|
313 if isinstance(ref, nodes.target): |
|
314 self.resolve_indirect_references(ref) |
|
315 for id in target['ids']: |
|
316 reflist = self.document.refids.get(id, []) |
|
317 if reflist: |
|
318 target.note_referenced_by(id=id) |
|
319 for ref in reflist: |
|
320 if ref.resolved: |
|
321 continue |
|
322 del ref['refid'] |
|
323 ref[attname] = attval |
|
324 if call_method: |
|
325 call_method(ref) |
|
326 ref.resolved = 1 |
|
327 if isinstance(ref, nodes.target): |
|
328 self.resolve_indirect_references(ref) |
|
329 |
|
330 |
|
331 class ExternalTargets(Transform): |
|
332 |
|
333 """ |
|
334 Given:: |
|
335 |
|
336 <paragraph> |
|
337 <reference refname="direct external"> |
|
338 direct external |
|
339 <target id="id1" name="direct external" refuri="http://direct"> |
|
340 |
|
341 The "refname" attribute is replaced by the direct "refuri" attribute:: |
|
342 |
|
343 <paragraph> |
|
344 <reference refuri="http://direct"> |
|
345 direct external |
|
346 <target id="id1" name="direct external" refuri="http://direct"> |
|
347 """ |
|
348 |
|
349 default_priority = 640 |
|
350 |
|
351 def apply(self): |
|
352 for target in self.document.traverse(nodes.target): |
|
353 if target.hasattr('refuri'): |
|
354 refuri = target['refuri'] |
|
355 for name in target['names']: |
|
356 reflist = self.document.refnames.get(name, []) |
|
357 if reflist: |
|
358 target.note_referenced_by(name=name) |
|
359 for ref in reflist: |
|
360 if ref.resolved: |
|
361 continue |
|
362 del ref['refname'] |
|
363 ref['refuri'] = refuri |
|
364 ref.resolved = 1 |
|
365 |
|
366 |
|
367 class InternalTargets(Transform): |
|
368 |
|
369 default_priority = 660 |
|
370 |
|
371 def apply(self): |
|
372 for target in self.document.traverse(nodes.target): |
|
373 if not target.hasattr('refuri') and not target.hasattr('refid'): |
|
374 self.resolve_reference_ids(target) |
|
375 |
|
376 def resolve_reference_ids(self, target): |
|
377 """ |
|
378 Given:: |
|
379 |
|
380 <paragraph> |
|
381 <reference refname="direct internal"> |
|
382 direct internal |
|
383 <target id="id1" name="direct internal"> |
|
384 |
|
385 The "refname" attribute is replaced by "refid" linking to the target's |
|
386 "id":: |
|
387 |
|
388 <paragraph> |
|
389 <reference refid="id1"> |
|
390 direct internal |
|
391 <target id="id1" name="direct internal"> |
|
392 """ |
|
393 for name in target['names']: |
|
394 refid = self.document.nameids[name] |
|
395 reflist = self.document.refnames.get(name, []) |
|
396 if reflist: |
|
397 target.note_referenced_by(name=name) |
|
398 for ref in reflist: |
|
399 if ref.resolved: |
|
400 continue |
|
401 del ref['refname'] |
|
402 ref['refid'] = refid |
|
403 ref.resolved = 1 |
|
404 |
|
405 |
|
406 class Footnotes(Transform): |
|
407 |
|
408 """ |
|
409 Assign numbers to autonumbered footnotes, and resolve links to footnotes, |
|
410 citations, and their references. |
|
411 |
|
412 Given the following ``document`` as input:: |
|
413 |
|
414 <document> |
|
415 <paragraph> |
|
416 A labeled autonumbered footnote referece: |
|
417 <footnote_reference auto="1" id="id1" refname="footnote"> |
|
418 <paragraph> |
|
419 An unlabeled autonumbered footnote referece: |
|
420 <footnote_reference auto="1" id="id2"> |
|
421 <footnote auto="1" id="id3"> |
|
422 <paragraph> |
|
423 Unlabeled autonumbered footnote. |
|
424 <footnote auto="1" id="footnote" name="footnote"> |
|
425 <paragraph> |
|
426 Labeled autonumbered footnote. |
|
427 |
|
428 Auto-numbered footnotes have attribute ``auto="1"`` and no label. |
|
429 Auto-numbered footnote_references have no reference text (they're |
|
430 empty elements). When resolving the numbering, a ``label`` element |
|
431 is added to the beginning of the ``footnote``, and reference text |
|
432 to the ``footnote_reference``. |
|
433 |
|
434 The transformed result will be:: |
|
435 |
|
436 <document> |
|
437 <paragraph> |
|
438 A labeled autonumbered footnote referece: |
|
439 <footnote_reference auto="1" id="id1" refid="footnote"> |
|
440 2 |
|
441 <paragraph> |
|
442 An unlabeled autonumbered footnote referece: |
|
443 <footnote_reference auto="1" id="id2" refid="id3"> |
|
444 1 |
|
445 <footnote auto="1" id="id3" backrefs="id2"> |
|
446 <label> |
|
447 1 |
|
448 <paragraph> |
|
449 Unlabeled autonumbered footnote. |
|
450 <footnote auto="1" id="footnote" name="footnote" backrefs="id1"> |
|
451 <label> |
|
452 2 |
|
453 <paragraph> |
|
454 Labeled autonumbered footnote. |
|
455 |
|
456 Note that the footnotes are not in the same order as the references. |
|
457 |
|
458 The labels and reference text are added to the auto-numbered ``footnote`` |
|
459 and ``footnote_reference`` elements. Footnote elements are backlinked to |
|
460 their references via "refids" attributes. References are assigned "id" |
|
461 and "refid" attributes. |
|
462 |
|
463 After adding labels and reference text, the "auto" attributes can be |
|
464 ignored. |
|
465 """ |
|
466 |
|
467 default_priority = 620 |
|
468 |
|
469 autofootnote_labels = None |
|
470 """Keep track of unlabeled autonumbered footnotes.""" |
|
471 |
|
472 symbols = [ |
|
473 # Entries 1-4 and 6 below are from section 12.51 of |
|
474 # The Chicago Manual of Style, 14th edition. |
|
475 '*', # asterisk/star |
|
476 u'\u2020', # dagger † |
|
477 u'\u2021', # double dagger ‡ |
|
478 u'\u00A7', # section mark § |
|
479 u'\u00B6', # paragraph mark (pilcrow) ¶ |
|
480 # (parallels ['||'] in CMoS) |
|
481 '#', # number sign |
|
482 # The entries below were chosen arbitrarily. |
|
483 u'\u2660', # spade suit ♠ |
|
484 u'\u2665', # heart suit ♥ |
|
485 u'\u2666', # diamond suit ♦ |
|
486 u'\u2663', # club suit ♣ |
|
487 ] |
|
488 |
|
489 def apply(self): |
|
490 self.autofootnote_labels = [] |
|
491 startnum = self.document.autofootnote_start |
|
492 self.document.autofootnote_start = self.number_footnotes(startnum) |
|
493 self.number_footnote_references(startnum) |
|
494 self.symbolize_footnotes() |
|
495 self.resolve_footnotes_and_citations() |
|
496 |
|
497 def number_footnotes(self, startnum): |
|
498 """ |
|
499 Assign numbers to autonumbered footnotes. |
|
500 |
|
501 For labeled autonumbered footnotes, copy the number over to |
|
502 corresponding footnote references. |
|
503 """ |
|
504 for footnote in self.document.autofootnotes: |
|
505 while 1: |
|
506 label = str(startnum) |
|
507 startnum += 1 |
|
508 if not self.document.nameids.has_key(label): |
|
509 break |
|
510 footnote.insert(0, nodes.label('', label)) |
|
511 for name in footnote['names']: |
|
512 for ref in self.document.footnote_refs.get(name, []): |
|
513 ref += nodes.Text(label) |
|
514 ref.delattr('refname') |
|
515 assert len(footnote['ids']) == len(ref['ids']) == 1 |
|
516 ref['refid'] = footnote['ids'][0] |
|
517 footnote.add_backref(ref['ids'][0]) |
|
518 self.document.note_refid(ref) |
|
519 ref.resolved = 1 |
|
520 if not footnote['names'] and not footnote['dupnames']: |
|
521 footnote['names'].append(label) |
|
522 self.document.note_explicit_target(footnote, footnote) |
|
523 self.autofootnote_labels.append(label) |
|
524 return startnum |
|
525 |
|
526 def number_footnote_references(self, startnum): |
|
527 """Assign numbers to autonumbered footnote references.""" |
|
528 i = 0 |
|
529 for ref in self.document.autofootnote_refs: |
|
530 if ref.resolved or ref.hasattr('refid'): |
|
531 continue |
|
532 try: |
|
533 label = self.autofootnote_labels[i] |
|
534 except IndexError: |
|
535 msg = self.document.reporter.error( |
|
536 'Too many autonumbered footnote references: only %s ' |
|
537 'corresponding footnotes available.' |
|
538 % len(self.autofootnote_labels), base_node=ref) |
|
539 msgid = self.document.set_id(msg) |
|
540 for ref in self.document.autofootnote_refs[i:]: |
|
541 if ref.resolved or ref.hasattr('refname'): |
|
542 continue |
|
543 prb = nodes.problematic( |
|
544 ref.rawsource, ref.rawsource, refid=msgid) |
|
545 prbid = self.document.set_id(prb) |
|
546 msg.add_backref(prbid) |
|
547 ref.replace_self(prb) |
|
548 break |
|
549 ref += nodes.Text(label) |
|
550 id = self.document.nameids[label] |
|
551 footnote = self.document.ids[id] |
|
552 ref['refid'] = id |
|
553 self.document.note_refid(ref) |
|
554 assert len(ref['ids']) == 1 |
|
555 footnote.add_backref(ref['ids'][0]) |
|
556 ref.resolved = 1 |
|
557 i += 1 |
|
558 |
|
559 def symbolize_footnotes(self): |
|
560 """Add symbols indexes to "[*]"-style footnotes and references.""" |
|
561 labels = [] |
|
562 for footnote in self.document.symbol_footnotes: |
|
563 reps, index = divmod(self.document.symbol_footnote_start, |
|
564 len(self.symbols)) |
|
565 labeltext = self.symbols[index] * (reps + 1) |
|
566 labels.append(labeltext) |
|
567 footnote.insert(0, nodes.label('', labeltext)) |
|
568 self.document.symbol_footnote_start += 1 |
|
569 self.document.set_id(footnote) |
|
570 i = 0 |
|
571 for ref in self.document.symbol_footnote_refs: |
|
572 try: |
|
573 ref += nodes.Text(labels[i]) |
|
574 except IndexError: |
|
575 msg = self.document.reporter.error( |
|
576 'Too many symbol footnote references: only %s ' |
|
577 'corresponding footnotes available.' % len(labels), |
|
578 base_node=ref) |
|
579 msgid = self.document.set_id(msg) |
|
580 for ref in self.document.symbol_footnote_refs[i:]: |
|
581 if ref.resolved or ref.hasattr('refid'): |
|
582 continue |
|
583 prb = nodes.problematic( |
|
584 ref.rawsource, ref.rawsource, refid=msgid) |
|
585 prbid = self.document.set_id(prb) |
|
586 msg.add_backref(prbid) |
|
587 ref.replace_self(prb) |
|
588 break |
|
589 footnote = self.document.symbol_footnotes[i] |
|
590 assert len(footnote['ids']) == 1 |
|
591 ref['refid'] = footnote['ids'][0] |
|
592 self.document.note_refid(ref) |
|
593 footnote.add_backref(ref['ids'][0]) |
|
594 i += 1 |
|
595 |
|
596 def resolve_footnotes_and_citations(self): |
|
597 """ |
|
598 Link manually-labeled footnotes and citations to/from their |
|
599 references. |
|
600 """ |
|
601 for footnote in self.document.footnotes: |
|
602 for label in footnote['names']: |
|
603 if self.document.footnote_refs.has_key(label): |
|
604 reflist = self.document.footnote_refs[label] |
|
605 self.resolve_references(footnote, reflist) |
|
606 for citation in self.document.citations: |
|
607 for label in citation['names']: |
|
608 if self.document.citation_refs.has_key(label): |
|
609 reflist = self.document.citation_refs[label] |
|
610 self.resolve_references(citation, reflist) |
|
611 |
|
612 def resolve_references(self, note, reflist): |
|
613 assert len(note['ids']) == 1 |
|
614 id = note['ids'][0] |
|
615 for ref in reflist: |
|
616 if ref.resolved: |
|
617 continue |
|
618 ref.delattr('refname') |
|
619 ref['refid'] = id |
|
620 assert len(ref['ids']) == 1 |
|
621 note.add_backref(ref['ids'][0]) |
|
622 ref.resolved = 1 |
|
623 note.resolved = 1 |
|
624 |
|
625 |
|
626 class CircularSubstitutionDefinitionError(Exception): pass |
|
627 |
|
628 |
|
629 class Substitutions(Transform): |
|
630 |
|
631 """ |
|
632 Given the following ``document`` as input:: |
|
633 |
|
634 <document> |
|
635 <paragraph> |
|
636 The |
|
637 <substitution_reference refname="biohazard"> |
|
638 biohazard |
|
639 symbol is deservedly scary-looking. |
|
640 <substitution_definition name="biohazard"> |
|
641 <image alt="biohazard" uri="biohazard.png"> |
|
642 |
|
643 The ``substitution_reference`` will simply be replaced by the |
|
644 contents of the corresponding ``substitution_definition``. |
|
645 |
|
646 The transformed result will be:: |
|
647 |
|
648 <document> |
|
649 <paragraph> |
|
650 The |
|
651 <image alt="biohazard" uri="biohazard.png"> |
|
652 symbol is deservedly scary-looking. |
|
653 <substitution_definition name="biohazard"> |
|
654 <image alt="biohazard" uri="biohazard.png"> |
|
655 """ |
|
656 |
|
657 default_priority = 220 |
|
658 """The Substitutions transform has to be applied very early, before |
|
659 `docutils.tranforms.frontmatter.DocTitle` and others.""" |
|
660 |
|
661 def apply(self): |
|
662 defs = self.document.substitution_defs |
|
663 normed = self.document.substitution_names |
|
664 subreflist = self.document.traverse(nodes.substitution_reference) |
|
665 nested = {} |
|
666 for ref in subreflist: |
|
667 refname = ref['refname'] |
|
668 key = None |
|
669 if defs.has_key(refname): |
|
670 key = refname |
|
671 else: |
|
672 normed_name = refname.lower() |
|
673 if normed.has_key(normed_name): |
|
674 key = normed[normed_name] |
|
675 if key is None: |
|
676 msg = self.document.reporter.error( |
|
677 'Undefined substitution referenced: "%s".' |
|
678 % refname, base_node=ref) |
|
679 msgid = self.document.set_id(msg) |
|
680 prb = nodes.problematic( |
|
681 ref.rawsource, ref.rawsource, refid=msgid) |
|
682 prbid = self.document.set_id(prb) |
|
683 msg.add_backref(prbid) |
|
684 ref.replace_self(prb) |
|
685 else: |
|
686 subdef = defs[key] |
|
687 parent = ref.parent |
|
688 index = parent.index(ref) |
|
689 if (subdef.attributes.has_key('ltrim') |
|
690 or subdef.attributes.has_key('trim')): |
|
691 if index > 0 and isinstance(parent[index - 1], |
|
692 nodes.Text): |
|
693 parent.replace(parent[index - 1], |
|
694 parent[index - 1].rstrip()) |
|
695 if (subdef.attributes.has_key('rtrim') |
|
696 or subdef.attributes.has_key('trim')): |
|
697 if (len(parent) > index + 1 |
|
698 and isinstance(parent[index + 1], nodes.Text)): |
|
699 parent.replace(parent[index + 1], |
|
700 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 subreflist.append(nested_ref) |
|
712 except CircularSubstitutionDefinitionError: |
|
713 parent = ref.parent |
|
714 if isinstance(parent, nodes.substitution_definition): |
|
715 msg = self.document.reporter.error( |
|
716 'Circular substitution definition detected:', |
|
717 nodes.literal_block(parent.rawsource, |
|
718 parent.rawsource), |
|
719 line=parent.line, base_node=parent) |
|
720 parent.replace_self(msg) |
|
721 else: |
|
722 msg = self.document.reporter.error( |
|
723 'Circular substitution definition referenced: "%s".' |
|
724 % refname, base_node=ref) |
|
725 msgid = self.document.set_id(msg) |
|
726 prb = nodes.problematic( |
|
727 ref.rawsource, ref.rawsource, refid=msgid) |
|
728 prbid = self.document.set_id(prb) |
|
729 msg.add_backref(prbid) |
|
730 ref.replace_self(prb) |
|
731 else: |
|
732 ref.replace_self(subdef_copy.children) |
|
733 |
|
734 |
|
735 class TargetNotes(Transform): |
|
736 |
|
737 """ |
|
738 Creates a footnote for each external target in the text, and corresponding |
|
739 footnote references after each reference. |
|
740 """ |
|
741 |
|
742 default_priority = 540 |
|
743 """The TargetNotes transform has to be applied after `IndirectHyperlinks` |
|
744 but before `Footnotes`.""" |
|
745 |
|
746 |
|
747 def __init__(self, document, startnode): |
|
748 Transform.__init__(self, document, startnode=startnode) |
|
749 |
|
750 self.classes = startnode.details.get('class', []) |
|
751 |
|
752 def apply(self): |
|
753 notes = {} |
|
754 nodelist = [] |
|
755 for target in self.document.traverse(nodes.target): |
|
756 # Only external targets. |
|
757 if not target.hasattr('refuri'): |
|
758 continue |
|
759 names = target['names'] |
|
760 refs = [] |
|
761 for name in names: |
|
762 refs.extend(self.document.refnames.get(name, [])) |
|
763 if not refs: |
|
764 continue |
|
765 footnote = self.make_target_footnote(target['refuri'], refs, |
|
766 notes) |
|
767 if not notes.has_key(target['refuri']): |
|
768 notes[target['refuri']] = footnote |
|
769 nodelist.append(footnote) |
|
770 # Take care of anonymous references. |
|
771 for ref in self.document.traverse(nodes.reference): |
|
772 if not ref.get('anonymous'): |
|
773 continue |
|
774 if ref.hasattr('refuri'): |
|
775 footnote = self.make_target_footnote(ref['refuri'], [ref], |
|
776 notes) |
|
777 if not notes.has_key(ref['refuri']): |
|
778 notes[ref['refuri']] = footnote |
|
779 nodelist.append(footnote) |
|
780 self.startnode.replace_self(nodelist) |
|
781 |
|
782 def make_target_footnote(self, refuri, refs, notes): |
|
783 if notes.has_key(refuri): # duplicate? |
|
784 footnote = notes[refuri] |
|
785 assert len(footnote['names']) == 1 |
|
786 footnote_name = footnote['names'][0] |
|
787 else: # original |
|
788 footnote = nodes.footnote() |
|
789 footnote_id = self.document.set_id(footnote) |
|
790 # Use uppercase letters and a colon; they can't be |
|
791 # produced inside names by the parser. |
|
792 footnote_name = 'TARGET_NOTE: ' + footnote_id |
|
793 footnote['auto'] = 1 |
|
794 footnote['names'] = [footnote_name] |
|
795 footnote_paragraph = nodes.paragraph() |
|
796 footnote_paragraph += nodes.reference('', refuri, refuri=refuri) |
|
797 footnote += footnote_paragraph |
|
798 self.document.note_autofootnote(footnote) |
|
799 self.document.note_explicit_target(footnote, footnote) |
|
800 for ref in refs: |
|
801 if isinstance(ref, nodes.target): |
|
802 continue |
|
803 refnode = nodes.footnote_reference( |
|
804 refname=footnote_name, auto=1) |
|
805 refnode['classes'] += self.classes |
|
806 self.document.note_autofootnote_ref(refnode) |
|
807 self.document.note_footnote_ref(refnode) |
|
808 index = ref.parent.index(ref) + 1 |
|
809 reflist = [refnode] |
|
810 if not utils.get_trim_footnote_ref_space(self.document.settings): |
|
811 if self.classes: |
|
812 reflist.insert(0, nodes.inline(text=' ', Classes=self.classes)) |
|
813 else: |
|
814 reflist.insert(0, nodes.Text(' ')) |
|
815 ref.parent.insert(index, reflist) |
|
816 return footnote |
|
817 |
|
818 |
|
819 class DanglingReferences(Transform): |
|
820 |
|
821 """ |
|
822 Check for dangling references (incl. footnote & citation) and for |
|
823 unreferenced targets. |
|
824 """ |
|
825 |
|
826 default_priority = 850 |
|
827 |
|
828 def apply(self): |
|
829 visitor = DanglingReferencesVisitor( |
|
830 self.document, |
|
831 self.document.transformer.unknown_reference_resolvers) |
|
832 self.document.walk(visitor) |
|
833 # *After* resolving all references, check for unreferenced |
|
834 # targets: |
|
835 for target in self.document.traverse(nodes.target): |
|
836 if not target.referenced: |
|
837 if target.get('anonymous'): |
|
838 # If we have unreferenced anonymous targets, there |
|
839 # is already an error message about anonymous |
|
840 # hyperlink mismatch; no need to generate another |
|
841 # message. |
|
842 continue |
|
843 if target['names']: |
|
844 naming = target['names'][0] |
|
845 elif target['ids']: |
|
846 naming = target['ids'][0] |
|
847 else: |
|
848 # Hack: Propagated targets always have their refid |
|
849 # attribute set. |
|
850 naming = target['refid'] |
|
851 self.document.reporter.info( |
|
852 'Hyperlink target "%s" is not referenced.' |
|
853 % naming, base_node=target) |
|
854 |
|
855 |
|
856 class DanglingReferencesVisitor(nodes.SparseNodeVisitor): |
|
857 |
|
858 def __init__(self, document, unknown_reference_resolvers): |
|
859 nodes.SparseNodeVisitor.__init__(self, document) |
|
860 self.document = document |
|
861 self.unknown_reference_resolvers = unknown_reference_resolvers |
|
862 |
|
863 def unknown_visit(self, node): |
|
864 pass |
|
865 |
|
866 def visit_reference(self, node): |
|
867 if node.resolved or not node.hasattr('refname'): |
|
868 return |
|
869 refname = node['refname'] |
|
870 id = self.document.nameids.get(refname) |
|
871 if id is None: |
|
872 for resolver_function in self.unknown_reference_resolvers: |
|
873 if resolver_function(node): |
|
874 break |
|
875 else: |
|
876 if self.document.nameids.has_key(refname): |
|
877 msg = self.document.reporter.error( |
|
878 'Duplicate target name, cannot be used as a unique ' |
|
879 'reference: "%s".' % (node['refname']), base_node=node) |
|
880 else: |
|
881 msg = self.document.reporter.error( |
|
882 'Unknown target name: "%s".' % (node['refname']), |
|
883 base_node=node) |
|
884 msgid = self.document.set_id(msg) |
|
885 prb = nodes.problematic( |
|
886 node.rawsource, node.rawsource, refid=msgid) |
|
887 prbid = self.document.set_id(prb) |
|
888 msg.add_backref(prbid) |
|
889 node.replace_self(prb) |
|
890 else: |
|
891 del node['refname'] |
|
892 node['refid'] = id |
|
893 self.document.ids[id].note_referenced_by(id=id) |
|
894 node.resolved = 1 |
|
895 |
|
896 visit_footnote_reference = visit_citation_reference = visit_reference |
|