16
16
from docutils.transforms import TransformError, Transform
19
indices = xrange(sys.maxint)
22
class ChainedTargets(Transform):
25
Attributes "refuri" and "refname" are migrated from the final direct
26
target up the chain of contiguous adjacent internal targets, using
27
`ChainedTargetResolver`.
30
default_priority = 420
19
class PropagateTargets(Transform):
22
Propagate empty internal targets to the next element.
24
Given the following nodes::
26
<target ids="internal1" names="internal1">
27
<target anonymous="1" ids="id1">
28
<target ids="internal2" names="internal2">
32
PropagateTargets propagates the ids and names of the internal
33
targets preceding the paragraph to the paragraph itself::
35
<target refid="internal1">
36
<target anonymous="1" refid="id1">
37
<target refid="internal2">
38
<paragraph ids="internal2 id1 internal1" names="internal2 internal1">
42
default_priority = 260
33
visitor = ChainedTargetResolver(self.document)
34
self.document.walk(visitor)
37
class ChainedTargetResolver(nodes.SparseNodeVisitor):
40
Copy reference attributes up the length of a hyperlink target chain.
42
"Chained targets" are multiple adjacent internal hyperlink targets which
43
"point to" an external or indirect target. After the transform, all
44
chained targets will effectively point to the same place.
46
Given the following ``document`` as input::
49
<target id="a" name="a">
50
<target id="b" name="b">
51
<target id="c" name="c" refuri="http://chained.external.targets">
52
<target id="d" name="d">
55
<target id="e" name="e">
57
<target id="f" name="f" refname="d">
59
``ChainedTargetResolver(document).walk()`` will transform the above into::
62
<target id="a" name="a" refuri="http://chained.external.targets">
63
<target id="b" name="b" refuri="http://chained.external.targets">
64
<target id="c" name="c" refuri="http://chained.external.targets">
65
<target id="d" name="d">
68
<target id="e" name="e" refname="d">
69
<target id="id1" refname="d">
70
<target id="f" name="f" refname="d">
73
def unknown_visit(self, node):
76
def visit_target(self, node):
77
if node.hasattr('refuri'):
79
call_if_named = self.document.note_external_target
80
elif node.hasattr('refname'):
82
call_if_named = self.document.note_indirect_target
83
elif node.hasattr('refid'):
88
attval = node[attname]
89
index = node.parent.index(node)
90
for i in range(index - 1, -1, -1):
91
sibling = node.parent[i]
92
if not isinstance(sibling, nodes.target) \
93
or sibling.hasattr('refuri') \
94
or sibling.hasattr('refname') \
95
or sibling.hasattr('refid'):
97
sibling[attname] = attval
98
if sibling.hasattr('name') and call_if_named:
99
call_if_named(sibling)
45
for target in self.document.traverse(nodes.target):
46
# Only block-level targets without reference (like ".. target:"):
47
if (isinstance(target.parent, nodes.TextElement) or
48
(target.hasattr('refid') or target.hasattr('refuri') or
49
target.hasattr('refname'))):
51
assert len(target) == 0, 'error: block-level target has children'
52
next_node = target.next_node(ascend=1)
53
# Do not move names and ids into Invisibles (we'd lose the
54
# attributes) or different Targetables (e.g. footnotes).
55
if (next_node is not None and
56
((not isinstance(next_node, nodes.Invisible) and
57
not isinstance(next_node, nodes.Targetable)) or
58
isinstance(next_node, nodes.target))):
59
next_node['ids'].extend(target['ids'])
60
next_node['names'].extend(target['names'])
61
# Set defaults for next_node.expect_referenced_by_name/id.
62
if not hasattr(next_node, 'expect_referenced_by_name'):
63
next_node.expect_referenced_by_name = {}
64
if not hasattr(next_node, 'expect_referenced_by_id'):
65
next_node.expect_referenced_by_id = {}
66
for id in target['ids']:
67
# Update IDs to node mapping.
68
self.document.ids[id] = next_node
69
# If next_node is referenced by id ``id``, this
70
# target shall be marked as referenced.
71
next_node.expect_referenced_by_id[id] = target
72
for name in target['names']:
73
next_node.expect_referenced_by_name[name] = target
74
# If there are any expect_referenced_by_... attributes
75
# in target set, copy them to next_node.
76
next_node.expect_referenced_by_name.update(
77
getattr(target, 'expect_referenced_by_name', {}))
78
next_node.expect_referenced_by_id.update(
79
getattr(target, 'expect_referenced_by_id', {}))
80
# Set refid to point to the first former ID of target
81
# which is now an ID of next_node.
82
target['refid'] = target['ids'][0]
83
# Clear ids and names; they have been moved to
87
self.document.note_refid(target)
102
90
class AnonymousHyperlinks(Transform):
120
108
<reference anonymous="1" refuri="http://external">
122
<target anonymous="1" id="id1">
123
<target anonymous="1" id="id2" refuri="http://external">
110
<target anonymous="1" ids="id1">
111
<target anonymous="1" ids="id2" refuri="http://external">
126
114
default_priority = 440
129
if len(self.document.anonymous_refs) \
130
!= len(self.document.anonymous_targets):
118
anonymous_targets = []
119
for node in self.document.traverse(nodes.reference):
120
if node.get('anonymous'):
121
anonymous_refs.append(node)
122
for node in self.document.traverse(nodes.target):
123
if node.get('anonymous'):
124
anonymous_targets.append(node)
125
if len(anonymous_refs) \
126
!= len(anonymous_targets):
131
127
msg = self.document.reporter.error(
132
128
'Anonymous hyperlink mismatch: %s references but %s '
133
129
'targets.\nSee "backrefs" attribute for IDs.'
134
% (len(self.document.anonymous_refs),
135
len(self.document.anonymous_targets)))
130
% (len(anonymous_refs), len(anonymous_targets)))
136
131
msgid = self.document.set_id(msg)
137
for ref in self.document.anonymous_refs:
132
for ref in anonymous_refs:
138
133
prb = nodes.problematic(
139
134
ref.rawsource, ref.rawsource, refid=msgid)
140
135
prbid = self.document.set_id(prb)
141
136
msg.add_backref(prbid)
142
ref.parent.replace(ref, prb)
137
ref.replace_self(prb)
144
for ref, target in zip(self.document.anonymous_refs,
145
self.document.anonymous_targets):
146
if target.hasattr('refuri'):
147
ref['refuri'] = target['refuri']
150
ref['refid'] = target['id']
151
self.document.note_refid(ref)
139
for ref, target in zip(anonymous_refs, anonymous_targets):
152
140
target.referenced = 1
142
if target.hasattr('refuri'):
143
ref['refuri'] = target['refuri']
147
if not target['ids']:
149
target = self.document.ids[target['refid']]
151
ref['refid'] = target['ids'][0]
152
self.document.note_refid(ref)
155
156
class IndirectHyperlinks(Transform):
213
214
self.resolve_indirect_references(target)
215
216
def resolve_indirect_target(self, target):
216
refname = target['refname']
217
reftarget_id = self.document.nameids.get(refname)
219
# Check the unknown_reference_resolvers
220
for resolver_function in (self.document.transformer
221
.unknown_reference_resolvers):
222
if resolver_function(target):
225
self.nonexistent_indirect_target(target)
217
refname = target.get('refname')
219
reftarget_id = target['refid']
221
reftarget_id = self.document.nameids.get(refname)
223
# Check the unknown_reference_resolvers
224
for resolver_function in \
225
self.document.transformer.unknown_reference_resolvers:
226
if resolver_function(target):
229
self.nonexistent_indirect_target(target)
227
231
reftarget = self.document.ids[reftarget_id]
232
reftarget.note_referenced_by(id=reftarget_id)
228
233
if isinstance(reftarget, nodes.target) \
229
and not reftarget.resolved and reftarget.hasattr('refname'):
234
and not reftarget.resolved and reftarget.hasattr('refname'):
230
235
if hasattr(target, 'multiply_indirect'):
231
236
#and target.multiply_indirect):
232
237
#del target.multiply_indirect
266
271
def indirect_target_error(self, target, explanation):
268
if target.hasattr('name'):
269
naming = '"%s" ' % target['name']
270
reflist = self.document.refnames.get(target['name'], [])
272
reflist = self.document.refids.get(target['id'], [])
273
naming += '(id="%s")' % target['id']
275
naming = '"%s" ' % target['names'][0]
276
for name in target['names']:
277
reflist.extend(self.document.refnames.get(name, []))
278
for id in target['ids']:
279
reflist.extend(self.document.refids.get(id, []))
280
naming += '(id="%s")' % target['ids'][0]
274
281
msg = self.document.reporter.error(
275
282
'Indirect hyperlink target %s refers to target "%s", %s.'
276
% (naming, target['refname'], explanation),
283
% (naming, target['refname'], explanation), base_node=target)
278
284
msgid = self.document.set_id(msg)
285
for ref in uniq(reflist):
280
286
prb = nodes.problematic(
281
287
ref.rawsource, ref.rawsource, refid=msgid)
282
288
prbid = self.document.set_id(prb)
283
289
msg.add_backref(prbid)
284
ref.parent.replace(ref, prb)
290
ref.replace_self(prb)
285
291
target.resolved = 1
287
293
def resolve_indirect_references(self, target):
288
294
if target.hasattr('refid'):
289
295
attname = 'refid'
291
296
call_method = self.document.note_refid
292
297
elif target.hasattr('refuri'):
293
298
attname = 'refuri'
295
call_method = self.document.note_external_target
298
302
attval = target[attname]
299
if target.hasattr('name'):
300
name = target['name']
302
reflist = self.document.refnames[name]
303
except KeyError, instance:
304
if target.referenced:
306
msg = self.document.reporter.info(
307
'Indirect hyperlink target "%s" is not referenced.'
308
% name, base_node=target)
309
target.referenced = 1
315
reflist = self.document.refids[id]
316
except KeyError, instance:
317
if target.referenced:
319
msg = self.document.reporter.info(
320
'Indirect hyperlink target id="%s" is not referenced.'
321
% id, base_node=target)
322
target.referenced = 1
329
ref[attname] = attval
330
if not call_if_named or ref.hasattr('name'):
333
if isinstance(ref, nodes.target):
334
self.resolve_indirect_references(ref)
335
target.referenced = 1
303
for name in target['names']:
304
reflist = self.document.refnames.get(name, [])
306
target.note_referenced_by(name=name)
311
ref[attname] = attval
315
if isinstance(ref, nodes.target):
316
self.resolve_indirect_references(ref)
317
for id in target['ids']:
318
reflist = self.document.refids.get(id, [])
320
target.note_referenced_by(id=id)
325
ref[attname] = attval
329
if isinstance(ref, nodes.target):
330
self.resolve_indirect_references(ref)
338
333
class ExternalTargets(Transform):
356
351
default_priority = 640
359
for target in self.document.external_targets:
360
if target.hasattr('refuri') and target.hasattr('name'):
361
name = target['name']
354
for target in self.document.traverse(nodes.target):
355
if target.hasattr('refuri'):
362
356
refuri = target['refuri']
364
reflist = self.document.refnames[name]
365
except KeyError, instance:
366
# @@@ First clause correct???
367
if not isinstance(target, nodes.target) or target.referenced:
369
msg = self.document.reporter.info(
370
'External hyperlink target "%s" is not referenced.'
371
% name, base_node=target)
372
target.referenced = 1
378
ref['refuri'] = refuri
380
target.referenced = 1
357
for name in target['names']:
358
reflist = self.document.refnames.get(name, [])
360
target.note_referenced_by(name=name)
365
ref['refuri'] = refuri
383
369
class InternalTargets(Transform):
389
<reference refname="direct internal">
391
<target id="id1" name="direct internal">
393
The "refname" attribute is replaced by "refid" linking to the target's
397
<reference refid="id1">
399
<target id="id1" name="direct internal">
402
371
default_priority = 660
405
for target in self.document.internal_targets:
406
if target.hasattr('refuri') or target.hasattr('refid') \
407
or not target.hasattr('name'):
409
name = target['name']
412
reflist = self.document.refnames[name]
413
except KeyError, instance:
414
if target.referenced:
416
msg = self.document.reporter.info(
417
'Internal hyperlink target "%s" is not referenced.'
418
% name, base_node=target)
419
target.referenced = 1
374
for target in self.document.traverse(nodes.target):
375
if not target.hasattr('refuri') and not target.hasattr('refid'):
376
self.resolve_reference_ids(target)
378
def resolve_reference_ids(self, target):
383
<reference refname="direct internal">
385
<target id="id1" name="direct internal">
387
The "refname" attribute is replaced by "refid" linking to the target's
391
<reference refid="id1">
393
<target id="id1" name="direct internal">
395
for name in target['names']:
396
refid = self.document.nameids[name]
397
reflist = self.document.refnames.get(name, [])
399
target.note_referenced_by(name=name)
421
400
for ref in reflist:
424
403
del ref['refname']
425
404
ref['refid'] = refid
427
target.referenced = 1
430
408
class Footnotes(Transform):
532
510
if not self.document.nameids.has_key(label):
534
512
footnote.insert(0, nodes.label('', label))
535
if footnote.hasattr('dupname'):
537
if footnote.hasattr('name'):
538
name = footnote['name']
513
for name in footnote['names']:
539
514
for ref in self.document.footnote_refs.get(name, []):
540
515
ref += nodes.Text(label)
541
516
ref.delattr('refname')
542
ref['refid'] = footnote['id']
543
footnote.add_backref(ref['id'])
517
assert len(footnote['ids']) == len(ref['ids']) == 1
518
ref['refid'] = footnote['ids'][0]
519
footnote.add_backref(ref['ids'][0])
544
520
self.document.note_refid(ref)
547
footnote['name'] = label
522
if not footnote['names'] and not footnote['dupnames']:
523
footnote['names'].append(label)
548
524
self.document.note_explicit_target(footnote, footnote)
549
525
self.autofootnote_labels.append(label)
625
603
for footnote in self.document.footnotes:
626
label = footnote['name']
627
if self.document.footnote_refs.has_key(label):
628
reflist = self.document.footnote_refs[label]
629
self.resolve_references(footnote, reflist)
604
for label in footnote['names']:
605
if self.document.footnote_refs.has_key(label):
606
reflist = self.document.footnote_refs[label]
607
self.resolve_references(footnote, reflist)
630
608
for citation in self.document.citations:
631
label = citation['name']
632
if self.document.citation_refs.has_key(label):
633
reflist = self.document.citation_refs[label]
634
self.resolve_references(citation, reflist)
609
for label in citation['names']:
610
if self.document.citation_refs.has_key(label):
611
reflist = self.document.citation_refs[label]
612
self.resolve_references(citation, reflist)
636
614
def resolve_references(self, note, reflist):
615
assert len(note['ids']) == 1
638
617
for ref in reflist:
641
620
ref.delattr('refname')
642
621
ref['refid'] = id
643
note.add_backref(ref['id'])
622
assert len(ref['ids']) == 1
623
note.add_backref(ref['ids'][0])
645
625
note.resolved = 1
628
class CircularSubstitutionDefinitionError(Exception): pass
648
631
class Substitutions(Transform):
681
664
defs = self.document.substitution_defs
682
665
normed = self.document.substitution_names
683
for refname, refs in self.document.substitution_refs.items():
686
if defs.has_key(refname):
689
normed_name = refname.lower()
690
if normed.has_key(normed_name):
691
key = normed[normed_name]
693
msg = self.document.reporter.error(
694
'Undefined substitution referenced: "%s".'
695
% refname, base_node=ref)
696
msgid = self.document.set_id(msg)
697
prb = nodes.problematic(
698
ref.rawsource, ref.rawsource, refid=msgid)
699
prbid = self.document.set_id(prb)
700
msg.add_backref(prbid)
701
ref.parent.replace(ref, prb)
703
ref.parent.replace(ref, defs[key].get_children())
704
self.document.substitution_refs = None # release replaced references
666
subreflist = self.document.traverse(nodes.substitution_reference)
668
for ref in subreflist:
669
refname = ref['refname']
671
if defs.has_key(refname):
674
normed_name = refname.lower()
675
if normed.has_key(normed_name):
676
key = normed[normed_name]
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)
690
index = parent.index(ref)
691
if (subdef.attributes.has_key('ltrim')
692
or subdef.attributes.has_key('trim')):
693
if index > 0 and isinstance(parent[index - 1],
695
parent.replace(parent[index - 1],
696
parent[index - 1].rstrip())
697
if (subdef.attributes.has_key('rtrim')
698
or subdef.attributes.has_key('trim')):
699
if (len(parent) > index + 1
700
and isinstance(parent[index + 1], nodes.Text)):
701
parent.replace(parent[index + 1],
702
parent[index + 1].lstrip())
703
subdef_copy = subdef.deepcopy()
705
# Take care of nested substitution references:
706
for nested_ref in subdef_copy.traverse(
707
nodes.substitution_reference):
708
nested_name = normed[nested_ref['refname'].lower()]
709
if nested_name in nested.setdefault(nested_name, []):
710
raise CircularSubstitutionDefinitionError
712
nested[nested_name].append(key)
713
subreflist.append(nested_ref)
714
except CircularSubstitutionDefinitionError:
716
if isinstance(parent, nodes.substitution_definition):
717
msg = self.document.reporter.error(
718
'Circular substitution definition detected:',
719
nodes.literal_block(parent.rawsource,
721
line=parent.line, base_node=parent)
722
parent.replace_self(msg)
724
msg = self.document.reporter.error(
725
'Circular substitution definition referenced: "%s".'
726
% refname, base_node=ref)
727
msgid = self.document.set_id(msg)
728
prb = nodes.problematic(
729
ref.rawsource, ref.rawsource, refid=msgid)
730
prbid = self.document.set_id(prb)
731
msg.add_backref(prbid)
732
ref.replace_self(prb)
734
ref.replace_self(subdef_copy.children)
707
737
class TargetNotes(Transform):
715
745
"""The TargetNotes transform has to be applied after `IndirectHyperlinks`
716
746
but before `Footnotes`."""
749
def __init__(self, document, startnode):
750
Transform.__init__(self, document, startnode=startnode)
752
self.classes = startnode.details.get('class', [])
721
for target in self.document.external_targets:
722
name = target.get('name')
724
print >>sys.stderr, 'no name on target: %r' % target
757
for target in self.document.traverse(nodes.target):
758
# Only external targets.
759
if not target.hasattr('refuri'):
726
refs = self.document.refnames.get(name, [])
761
names = target['names']
764
refs.extend(self.document.refnames.get(name, []))
729
footnote = self.make_target_footnote(target, refs, notes)
767
footnote = self.make_target_footnote(target['refuri'], refs,
730
769
if not notes.has_key(target['refuri']):
731
770
notes[target['refuri']] = footnote
732
771
nodelist.append(footnote)
733
if len(self.document.anonymous_targets) \
734
== len(self.document.anonymous_refs):
735
for target, ref in zip(self.document.anonymous_targets,
736
self.document.anonymous_refs):
737
if target.hasattr('refuri'):
738
footnote = self.make_target_footnote(target, [ref], notes)
739
if not notes.has_key(target['refuri']):
740
notes[target['refuri']] = footnote
741
nodelist.append(footnote)
742
self.startnode.parent.replace(self.startnode, nodelist)
772
# Take care of anonymous references.
773
for ref in self.document.traverse(nodes.reference):
774
if not ref.get('anonymous'):
776
if ref.hasattr('refuri'):
777
footnote = self.make_target_footnote(ref['refuri'], [ref],
779
if not notes.has_key(ref['refuri']):
780
notes[ref['refuri']] = footnote
781
nodelist.append(footnote)
782
self.startnode.replace_self(nodelist)
744
def make_target_footnote(self, target, refs, notes):
745
refuri = target['refuri']
784
def make_target_footnote(self, refuri, refs, notes):
746
785
if notes.has_key(refuri): # duplicate?
747
786
footnote = notes[refuri]
748
footnote_name = footnote['name']
787
assert len(footnote['names']) == 1
788
footnote_name = footnote['names'][0]
750
790
footnote = nodes.footnote()
751
791
footnote_id = self.document.set_id(footnote)
752
# Use a colon; they can't be produced inside names by the parser:
753
footnote_name = 'target_note: ' + footnote_id
792
# Use uppercase letters and a colon; they can't be
793
# produced inside names by the parser.
794
footnote_name = 'TARGET_NOTE: ' + footnote_id
754
795
footnote['auto'] = 1
755
footnote['name'] = footnote_name
796
footnote['names'] = [footnote_name]
756
797
footnote_paragraph = nodes.paragraph()
757
798
footnote_paragraph += nodes.reference('', refuri, refuri=refuri)
758
799
footnote += footnote_paragraph
764
805
refnode = nodes.footnote_reference(
765
806
refname=footnote_name, auto=1)
807
refnode['classes'] += self.classes
766
808
self.document.note_autofootnote_ref(refnode)
767
809
self.document.note_footnote_ref(refnode)
768
810
index = ref.parent.index(ref) + 1
769
811
reflist = [refnode]
770
if not self.document.settings.trim_footnote_reference_space:
771
reflist.insert(0, nodes.Text(' '))
812
if not utils.get_trim_footnote_ref_space(self.document.settings):
814
reflist.insert(0, nodes.inline(text=' ', Classes=self.classes))
816
reflist.insert(0, nodes.Text(' '))
772
817
ref.parent.insert(index, reflist)
821
class DanglingReferences(Transform):
824
Check for dangling references (incl. footnote & citation) and for
825
unreferenced targets.
828
default_priority = 850
831
visitor = DanglingReferencesVisitor(
833
self.document.transformer.unknown_reference_resolvers)
834
self.document.walk(visitor)
835
# *After* resolving all references, check for unreferenced
837
for target in self.document.traverse(nodes.target):
838
if not target.referenced:
839
if target.get('anonymous'):
840
# If we have unreferenced anonymous targets, there
841
# is already an error message about anonymous
842
# hyperlink mismatch; no need to generate another
846
naming = target['names'][0]
848
naming = target['ids'][0]
850
# Hack: Propagated targets always have their refid
852
naming = target['refid']
853
self.document.reporter.info(
854
'Hyperlink target "%s" is not referenced.'
855
% naming, base_node=target)
858
class DanglingReferencesVisitor(nodes.SparseNodeVisitor):
860
def __init__(self, document, unknown_reference_resolvers):
861
nodes.SparseNodeVisitor.__init__(self, document)
862
self.document = document
863
self.unknown_reference_resolvers = unknown_reference_resolvers
865
def unknown_visit(self, node):
868
def visit_reference(self, node):
869
if node.resolved or not node.hasattr('refname'):
871
refname = node['refname']
872
id = self.document.nameids.get(refname)
874
for resolver_function in self.unknown_reference_resolvers:
875
if resolver_function(node):
878
if self.document.nameids.has_key(refname):
879
msg = self.document.reporter.error(
880
'Duplicate target name, cannot be used as a unique '
881
'reference: "%s".' % (node['refname']), base_node=node)
883
msg = self.document.reporter.error(
884
'Unknown target name: "%s".' % (node['refname']),
886
msgid = self.document.set_id(msg)
887
prb = nodes.problematic(
888
node.rawsource, node.rawsource, refid=msgid)
889
prbid = self.document.set_id(prb)
890
msg.add_backref(prbid)
891
node.replace_self(prb)
895
self.document.ids[id].note_referenced_by(id=id)
898
visit_footnote_reference = visit_citation_reference = visit_reference