references.py 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919
  1. # $Id: references.py 9037 2022-03-05 23:31:10Z milde $
  2. # Author: David Goodger <goodger@python.org>
  3. # Copyright: This module has been placed in the public domain.
  4. """
  5. Transforms for resolving references.
  6. """
  7. __docformat__ = 'reStructuredText'
  8. from docutils import nodes, utils
  9. from docutils.transforms import Transform
  10. class PropagateTargets(Transform):
  11. """
  12. Propagate empty internal targets to the next element.
  13. Given the following nodes::
  14. <target ids="internal1" names="internal1">
  15. <target anonymous="1" ids="id1">
  16. <target ids="internal2" names="internal2">
  17. <paragraph>
  18. This is a test.
  19. PropagateTargets propagates the ids and names of the internal
  20. targets preceding the paragraph to the paragraph itself::
  21. <target refid="internal1">
  22. <target anonymous="1" refid="id1">
  23. <target refid="internal2">
  24. <paragraph ids="internal2 id1 internal1" names="internal2 internal1">
  25. This is a test.
  26. """
  27. default_priority = 260
  28. def apply(self):
  29. for target in self.document.findall(nodes.target):
  30. # Only block-level targets without reference (like ".. _target:"):
  31. if (isinstance(target.parent, nodes.TextElement)
  32. or (target.hasattr('refid') or target.hasattr('refuri')
  33. or target.hasattr('refname'))):
  34. continue
  35. assert len(target) == 0, 'error: block-level target has children'
  36. next_node = target.next_node(ascend=True)
  37. # skip system messages (may be removed by universal.FilterMessages)
  38. while isinstance(next_node, nodes.system_message):
  39. next_node = next_node.next_node(ascend=True, descend=False)
  40. # Do not move names and ids into Invisibles (we'd lose the
  41. # attributes) or different Targetables (e.g. footnotes).
  42. if (next_node is None
  43. or isinstance(next_node, (nodes.Invisible, nodes.Targetable))
  44. and not isinstance(next_node, nodes.target)):
  45. continue
  46. next_node['ids'].extend(target['ids'])
  47. next_node['names'].extend(target['names'])
  48. # Set defaults for next_node.expect_referenced_by_name/id.
  49. if not hasattr(next_node, 'expect_referenced_by_name'):
  50. next_node.expect_referenced_by_name = {}
  51. if not hasattr(next_node, 'expect_referenced_by_id'):
  52. next_node.expect_referenced_by_id = {}
  53. for id in target['ids']:
  54. # Update IDs to node mapping.
  55. self.document.ids[id] = next_node
  56. # If next_node is referenced by id ``id``, this
  57. # target shall be marked as referenced.
  58. next_node.expect_referenced_by_id[id] = target
  59. for name in target['names']:
  60. next_node.expect_referenced_by_name[name] = target
  61. # If there are any expect_referenced_by_... attributes
  62. # in target set, copy them to next_node.
  63. next_node.expect_referenced_by_name.update(
  64. getattr(target, 'expect_referenced_by_name', {}))
  65. next_node.expect_referenced_by_id.update(
  66. getattr(target, 'expect_referenced_by_id', {}))
  67. # Set refid to point to the first former ID of target
  68. # which is now an ID of next_node.
  69. target['refid'] = target['ids'][0]
  70. # Clear ids and names; they have been moved to
  71. # next_node.
  72. target['ids'] = []
  73. target['names'] = []
  74. self.document.note_refid(target)
  75. class AnonymousHyperlinks(Transform):
  76. """
  77. Link anonymous references to targets. Given::
  78. <paragraph>
  79. <reference anonymous="1">
  80. internal
  81. <reference anonymous="1">
  82. external
  83. <target anonymous="1" ids="id1">
  84. <target anonymous="1" ids="id2" refuri="http://external">
  85. Corresponding references are linked via "refid" or resolved via "refuri"::
  86. <paragraph>
  87. <reference anonymous="1" refid="id1">
  88. text
  89. <reference anonymous="1" refuri="http://external">
  90. external
  91. <target anonymous="1" ids="id1">
  92. <target anonymous="1" ids="id2" refuri="http://external">
  93. """
  94. default_priority = 440
  95. def apply(self):
  96. anonymous_refs = []
  97. anonymous_targets = []
  98. for node in self.document.findall(nodes.reference):
  99. if node.get('anonymous'):
  100. anonymous_refs.append(node)
  101. for node in self.document.findall(nodes.target):
  102. if node.get('anonymous'):
  103. anonymous_targets.append(node)
  104. if len(anonymous_refs) != len(anonymous_targets):
  105. msg = self.document.reporter.error(
  106. 'Anonymous hyperlink mismatch: %s references but %s '
  107. 'targets.\nSee "backrefs" attribute for IDs.'
  108. % (len(anonymous_refs), len(anonymous_targets)))
  109. msgid = self.document.set_id(msg)
  110. for ref in anonymous_refs:
  111. prb = nodes.problematic(
  112. ref.rawsource, ref.rawsource, refid=msgid)
  113. prbid = self.document.set_id(prb)
  114. msg.add_backref(prbid)
  115. ref.replace_self(prb)
  116. return
  117. for ref, target in zip(anonymous_refs, anonymous_targets):
  118. target.referenced = 1
  119. while True:
  120. if target.hasattr('refuri'):
  121. ref['refuri'] = target['refuri']
  122. ref.resolved = 1
  123. break
  124. else:
  125. if not target['ids']:
  126. # Propagated target.
  127. target = self.document.ids[target['refid']]
  128. continue
  129. ref['refid'] = target['ids'][0]
  130. self.document.note_refid(ref)
  131. break
  132. class IndirectHyperlinks(Transform):
  133. """
  134. a) Indirect external references::
  135. <paragraph>
  136. <reference refname="indirect external">
  137. indirect external
  138. <target id="id1" name="direct external"
  139. refuri="http://indirect">
  140. <target id="id2" name="indirect external"
  141. refname="direct external">
  142. The "refuri" attribute is migrated back to all indirect targets
  143. from the final direct target (i.e. a target not referring to
  144. another indirect target)::
  145. <paragraph>
  146. <reference refname="indirect external">
  147. indirect external
  148. <target id="id1" name="direct external"
  149. refuri="http://indirect">
  150. <target id="id2" name="indirect external"
  151. refuri="http://indirect">
  152. Once the attribute is migrated, the preexisting "refname" attribute
  153. is dropped.
  154. b) Indirect internal references::
  155. <target id="id1" name="final target">
  156. <paragraph>
  157. <reference refname="indirect internal">
  158. indirect internal
  159. <target id="id2" name="indirect internal 2"
  160. refname="final target">
  161. <target id="id3" name="indirect internal"
  162. refname="indirect internal 2">
  163. Targets which indirectly refer to an internal target become one-hop
  164. indirect (their "refid" attributes are directly set to the internal
  165. target's "id"). References which indirectly refer to an internal
  166. target become direct internal references::
  167. <target id="id1" name="final target">
  168. <paragraph>
  169. <reference refid="id1">
  170. indirect internal
  171. <target id="id2" name="indirect internal 2" refid="id1">
  172. <target id="id3" name="indirect internal" refid="id1">
  173. """
  174. default_priority = 460
  175. def apply(self):
  176. for target in self.document.indirect_targets:
  177. if not target.resolved:
  178. self.resolve_indirect_target(target)
  179. self.resolve_indirect_references(target)
  180. def resolve_indirect_target(self, target):
  181. refname = target.get('refname')
  182. if refname is None:
  183. reftarget_id = target['refid']
  184. else:
  185. reftarget_id = self.document.nameids.get(refname)
  186. if not reftarget_id:
  187. # Check the unknown_reference_resolvers
  188. for resolver_function in \
  189. self.document.transformer.unknown_reference_resolvers:
  190. if resolver_function(target):
  191. break
  192. else:
  193. self.nonexistent_indirect_target(target)
  194. return
  195. reftarget = self.document.ids[reftarget_id]
  196. reftarget.note_referenced_by(id=reftarget_id)
  197. if (isinstance(reftarget, nodes.target)
  198. and not reftarget.resolved
  199. and reftarget.hasattr('refname')):
  200. if hasattr(target, 'multiply_indirect'):
  201. self.circular_indirect_reference(target)
  202. return
  203. target.multiply_indirect = 1
  204. self.resolve_indirect_target(reftarget) # multiply indirect
  205. del target.multiply_indirect
  206. if reftarget.hasattr('refuri'):
  207. target['refuri'] = reftarget['refuri']
  208. if 'refid' in target:
  209. del target['refid']
  210. elif reftarget.hasattr('refid'):
  211. target['refid'] = reftarget['refid']
  212. self.document.note_refid(target)
  213. else:
  214. if reftarget['ids']:
  215. target['refid'] = reftarget_id
  216. self.document.note_refid(target)
  217. else:
  218. self.nonexistent_indirect_target(target)
  219. return
  220. if refname is not None:
  221. del target['refname']
  222. target.resolved = 1
  223. def nonexistent_indirect_target(self, target):
  224. if target['refname'] in self.document.nameids:
  225. self.indirect_target_error(target, 'which is a duplicate, and '
  226. 'cannot be used as a unique reference')
  227. else:
  228. self.indirect_target_error(target, 'which does not exist')
  229. def circular_indirect_reference(self, target):
  230. self.indirect_target_error(target, 'forming a circular reference')
  231. def indirect_target_error(self, target, explanation):
  232. naming = ''
  233. reflist = []
  234. if target['names']:
  235. naming = '"%s" ' % target['names'][0]
  236. for name in target['names']:
  237. reflist.extend(self.document.refnames.get(name, []))
  238. for id in target['ids']:
  239. reflist.extend(self.document.refids.get(id, []))
  240. if target['ids']:
  241. naming += '(id="%s")' % target['ids'][0]
  242. msg = self.document.reporter.error(
  243. 'Indirect hyperlink target %s refers to target "%s", %s.'
  244. % (naming, target['refname'], explanation), base_node=target)
  245. msgid = self.document.set_id(msg)
  246. for ref in utils.uniq(reflist):
  247. prb = nodes.problematic(
  248. ref.rawsource, ref.rawsource, refid=msgid)
  249. prbid = self.document.set_id(prb)
  250. msg.add_backref(prbid)
  251. ref.replace_self(prb)
  252. target.resolved = 1
  253. def resolve_indirect_references(self, target):
  254. if target.hasattr('refid'):
  255. attname = 'refid'
  256. call_method = self.document.note_refid
  257. elif target.hasattr('refuri'):
  258. attname = 'refuri'
  259. call_method = None
  260. else:
  261. return
  262. attval = target[attname]
  263. for name in target['names']:
  264. reflist = self.document.refnames.get(name, [])
  265. if reflist:
  266. target.note_referenced_by(name=name)
  267. for ref in reflist:
  268. if ref.resolved:
  269. continue
  270. del ref['refname']
  271. ref[attname] = attval
  272. if call_method:
  273. call_method(ref)
  274. ref.resolved = 1
  275. if isinstance(ref, nodes.target):
  276. self.resolve_indirect_references(ref)
  277. for id in target['ids']:
  278. reflist = self.document.refids.get(id, [])
  279. if reflist:
  280. target.note_referenced_by(id=id)
  281. for ref in reflist:
  282. if ref.resolved:
  283. continue
  284. del ref['refid']
  285. ref[attname] = attval
  286. if call_method:
  287. call_method(ref)
  288. ref.resolved = 1
  289. if isinstance(ref, nodes.target):
  290. self.resolve_indirect_references(ref)
  291. class ExternalTargets(Transform):
  292. """
  293. Given::
  294. <paragraph>
  295. <reference refname="direct external">
  296. direct external
  297. <target id="id1" name="direct external" refuri="http://direct">
  298. The "refname" attribute is replaced by the direct "refuri" attribute::
  299. <paragraph>
  300. <reference refuri="http://direct">
  301. direct external
  302. <target id="id1" name="direct external" refuri="http://direct">
  303. """
  304. default_priority = 640
  305. def apply(self):
  306. for target in self.document.findall(nodes.target):
  307. if target.hasattr('refuri'):
  308. refuri = target['refuri']
  309. for name in target['names']:
  310. reflist = self.document.refnames.get(name, [])
  311. if reflist:
  312. target.note_referenced_by(name=name)
  313. for ref in reflist:
  314. if ref.resolved:
  315. continue
  316. del ref['refname']
  317. ref['refuri'] = refuri
  318. ref.resolved = 1
  319. class InternalTargets(Transform):
  320. default_priority = 660
  321. def apply(self):
  322. for target in self.document.findall(nodes.target):
  323. if not target.hasattr('refuri') and not target.hasattr('refid'):
  324. self.resolve_reference_ids(target)
  325. def resolve_reference_ids(self, target):
  326. """
  327. Given::
  328. <paragraph>
  329. <reference refname="direct internal">
  330. direct internal
  331. <target id="id1" name="direct internal">
  332. The "refname" attribute is replaced by "refid" linking to the target's
  333. "id"::
  334. <paragraph>
  335. <reference refid="id1">
  336. direct internal
  337. <target id="id1" name="direct internal">
  338. """
  339. for name in target['names']:
  340. refid = self.document.nameids.get(name)
  341. reflist = self.document.refnames.get(name, [])
  342. if reflist:
  343. target.note_referenced_by(name=name)
  344. for ref in reflist:
  345. if ref.resolved:
  346. continue
  347. if refid:
  348. del ref['refname']
  349. ref['refid'] = refid
  350. ref.resolved = 1
  351. class Footnotes(Transform):
  352. """
  353. Assign numbers to autonumbered footnotes, and resolve links to footnotes,
  354. citations, and their references.
  355. Given the following ``document`` as input::
  356. <document>
  357. <paragraph>
  358. A labeled autonumbered footnote reference:
  359. <footnote_reference auto="1" id="id1" refname="footnote">
  360. <paragraph>
  361. An unlabeled autonumbered footnote reference:
  362. <footnote_reference auto="1" id="id2">
  363. <footnote auto="1" id="id3">
  364. <paragraph>
  365. Unlabeled autonumbered footnote.
  366. <footnote auto="1" id="footnote" name="footnote">
  367. <paragraph>
  368. Labeled autonumbered footnote.
  369. Auto-numbered footnotes have attribute ``auto="1"`` and no label.
  370. Auto-numbered footnote_references have no reference text (they're
  371. empty elements). When resolving the numbering, a ``label`` element
  372. is added to the beginning of the ``footnote``, and reference text
  373. to the ``footnote_reference``.
  374. The transformed result will be::
  375. <document>
  376. <paragraph>
  377. A labeled autonumbered footnote reference:
  378. <footnote_reference auto="1" id="id1" refid="footnote">
  379. 2
  380. <paragraph>
  381. An unlabeled autonumbered footnote reference:
  382. <footnote_reference auto="1" id="id2" refid="id3">
  383. 1
  384. <footnote auto="1" id="id3" backrefs="id2">
  385. <label>
  386. 1
  387. <paragraph>
  388. Unlabeled autonumbered footnote.
  389. <footnote auto="1" id="footnote" name="footnote" backrefs="id1">
  390. <label>
  391. 2
  392. <paragraph>
  393. Labeled autonumbered footnote.
  394. Note that the footnotes are not in the same order as the references.
  395. The labels and reference text are added to the auto-numbered ``footnote``
  396. and ``footnote_reference`` elements. Footnote elements are backlinked to
  397. their references via "refids" attributes. References are assigned "id"
  398. and "refid" attributes.
  399. After adding labels and reference text, the "auto" attributes can be
  400. ignored.
  401. """
  402. default_priority = 620
  403. autofootnote_labels = None
  404. """Keep track of unlabeled autonumbered footnotes."""
  405. symbols = [
  406. # Entries 1-4 and 6 below are from section 12.51 of
  407. # The Chicago Manual of Style, 14th edition.
  408. '*', # asterisk/star
  409. '\u2020', # † &dagger; dagger
  410. '\u2021', # ‡ &Dagger; double dagger
  411. '\u00A7', # § &sect; section mark
  412. '\u00B6', # ¶ &para; paragraph mark (pilcrow)
  413. # (parallels ['||'] in CMoS)
  414. '#', # number sign
  415. # The entries below were chosen arbitrarily.
  416. '\u2660', # ♠ &spades; spade suit
  417. '\u2665', # ♡ &hearts; heart suit
  418. '\u2666', # ♢ &diams; diamond suit
  419. '\u2663', # ♣ &clubs; club suit
  420. ]
  421. def apply(self):
  422. self.autofootnote_labels = []
  423. startnum = self.document.autofootnote_start
  424. self.document.autofootnote_start = self.number_footnotes(startnum)
  425. self.number_footnote_references(startnum)
  426. self.symbolize_footnotes()
  427. self.resolve_footnotes_and_citations()
  428. def number_footnotes(self, startnum):
  429. """
  430. Assign numbers to autonumbered footnotes.
  431. For labeled autonumbered footnotes, copy the number over to
  432. corresponding footnote references.
  433. """
  434. for footnote in self.document.autofootnotes:
  435. while True:
  436. label = str(startnum)
  437. startnum += 1
  438. if label not in self.document.nameids:
  439. break
  440. footnote.insert(0, nodes.label('', label))
  441. for name in footnote['names']:
  442. for ref in self.document.footnote_refs.get(name, []):
  443. ref += nodes.Text(label)
  444. ref.delattr('refname')
  445. assert len(footnote['ids']) == len(ref['ids']) == 1
  446. ref['refid'] = footnote['ids'][0]
  447. footnote.add_backref(ref['ids'][0])
  448. self.document.note_refid(ref)
  449. ref.resolved = 1
  450. if not footnote['names'] and not footnote['dupnames']:
  451. footnote['names'].append(label)
  452. self.document.note_explicit_target(footnote, footnote)
  453. self.autofootnote_labels.append(label)
  454. return startnum
  455. def number_footnote_references(self, startnum):
  456. """Assign numbers to autonumbered footnote references."""
  457. i = 0
  458. for ref in self.document.autofootnote_refs:
  459. if ref.resolved or ref.hasattr('refid'):
  460. continue
  461. try:
  462. label = self.autofootnote_labels[i]
  463. except IndexError:
  464. msg = self.document.reporter.error(
  465. 'Too many autonumbered footnote references: only %s '
  466. 'corresponding footnotes available.'
  467. % len(self.autofootnote_labels), base_node=ref)
  468. msgid = self.document.set_id(msg)
  469. for ref in self.document.autofootnote_refs[i:]:
  470. if ref.resolved or ref.hasattr('refname'):
  471. continue
  472. prb = nodes.problematic(
  473. ref.rawsource, ref.rawsource, refid=msgid)
  474. prbid = self.document.set_id(prb)
  475. msg.add_backref(prbid)
  476. ref.replace_self(prb)
  477. break
  478. ref += nodes.Text(label)
  479. id = self.document.nameids[label]
  480. footnote = self.document.ids[id]
  481. ref['refid'] = id
  482. self.document.note_refid(ref)
  483. assert len(ref['ids']) == 1
  484. footnote.add_backref(ref['ids'][0])
  485. ref.resolved = 1
  486. i += 1
  487. def symbolize_footnotes(self):
  488. """Add symbols indexes to "[*]"-style footnotes and references."""
  489. labels = []
  490. for footnote in self.document.symbol_footnotes:
  491. reps, index = divmod(self.document.symbol_footnote_start,
  492. len(self.symbols))
  493. labeltext = self.symbols[index] * (reps + 1)
  494. labels.append(labeltext)
  495. footnote.insert(0, nodes.label('', labeltext))
  496. self.document.symbol_footnote_start += 1
  497. self.document.set_id(footnote)
  498. i = 0
  499. for ref in self.document.symbol_footnote_refs:
  500. try:
  501. ref += nodes.Text(labels[i])
  502. except IndexError:
  503. msg = self.document.reporter.error(
  504. 'Too many symbol footnote references: only %s '
  505. 'corresponding footnotes available.' % len(labels),
  506. base_node=ref)
  507. msgid = self.document.set_id(msg)
  508. for ref in self.document.symbol_footnote_refs[i:]:
  509. if ref.resolved or ref.hasattr('refid'):
  510. continue
  511. prb = nodes.problematic(
  512. ref.rawsource, ref.rawsource, refid=msgid)
  513. prbid = self.document.set_id(prb)
  514. msg.add_backref(prbid)
  515. ref.replace_self(prb)
  516. break
  517. footnote = self.document.symbol_footnotes[i]
  518. assert len(footnote['ids']) == 1
  519. ref['refid'] = footnote['ids'][0]
  520. self.document.note_refid(ref)
  521. footnote.add_backref(ref['ids'][0])
  522. i += 1
  523. def resolve_footnotes_and_citations(self):
  524. """
  525. Link manually-labeled footnotes and citations to/from their
  526. references.
  527. """
  528. for footnote in self.document.footnotes:
  529. for label in footnote['names']:
  530. if label in self.document.footnote_refs:
  531. reflist = self.document.footnote_refs[label]
  532. self.resolve_references(footnote, reflist)
  533. for citation in self.document.citations:
  534. for label in citation['names']:
  535. if label in self.document.citation_refs:
  536. reflist = self.document.citation_refs[label]
  537. self.resolve_references(citation, reflist)
  538. def resolve_references(self, note, reflist):
  539. assert len(note['ids']) == 1
  540. id = note['ids'][0]
  541. for ref in reflist:
  542. if ref.resolved:
  543. continue
  544. ref.delattr('refname')
  545. ref['refid'] = id
  546. assert len(ref['ids']) == 1
  547. note.add_backref(ref['ids'][0])
  548. ref.resolved = 1
  549. note.resolved = 1
  550. class CircularSubstitutionDefinitionError(Exception):
  551. pass
  552. class Substitutions(Transform):
  553. """
  554. Given the following ``document`` as input::
  555. <document>
  556. <paragraph>
  557. The
  558. <substitution_reference refname="biohazard">
  559. biohazard
  560. symbol is deservedly scary-looking.
  561. <substitution_definition name="biohazard">
  562. <image alt="biohazard" uri="biohazard.png">
  563. The ``substitution_reference`` will simply be replaced by the
  564. contents of the corresponding ``substitution_definition``.
  565. The transformed result will be::
  566. <document>
  567. <paragraph>
  568. The
  569. <image alt="biohazard" uri="biohazard.png">
  570. symbol is deservedly scary-looking.
  571. <substitution_definition name="biohazard">
  572. <image alt="biohazard" uri="biohazard.png">
  573. """
  574. default_priority = 220
  575. """The Substitutions transform has to be applied very early, before
  576. `docutils.transforms.frontmatter.DocTitle` and others."""
  577. def apply(self):
  578. defs = self.document.substitution_defs
  579. normed = self.document.substitution_names
  580. nested = {}
  581. line_length_limit = getattr(self.document.settings,
  582. "line_length_limit", 10000)
  583. subreflist = list(self.document.findall(nodes.substitution_reference))
  584. for ref in subreflist:
  585. msg = ''
  586. refname = ref['refname']
  587. if refname in defs:
  588. key = refname
  589. else:
  590. normed_name = refname.lower()
  591. key = normed.get(normed_name, None)
  592. if key is None:
  593. msg = self.document.reporter.error(
  594. 'Undefined substitution referenced: "%s".'
  595. % refname, base_node=ref)
  596. else:
  597. subdef = defs[key]
  598. if len(subdef.astext()) > line_length_limit:
  599. msg = self.document.reporter.error(
  600. 'Substitution definition "%s" exceeds the'
  601. ' line-length-limit.' % key)
  602. if msg:
  603. msgid = self.document.set_id(msg)
  604. prb = nodes.problematic(
  605. ref.rawsource, ref.rawsource, refid=msgid)
  606. prbid = self.document.set_id(prb)
  607. msg.add_backref(prbid)
  608. ref.replace_self(prb)
  609. continue
  610. parent = ref.parent
  611. index = parent.index(ref)
  612. if ('ltrim' in subdef.attributes
  613. or 'trim' in subdef.attributes):
  614. if index > 0 and isinstance(parent[index - 1],
  615. nodes.Text):
  616. parent[index - 1] = parent[index - 1].rstrip()
  617. if ('rtrim' in subdef.attributes
  618. or 'trim' in subdef.attributes):
  619. if (len(parent) > index + 1
  620. and isinstance(parent[index + 1], nodes.Text)):
  621. parent[index + 1] = parent[index + 1].lstrip()
  622. subdef_copy = subdef.deepcopy()
  623. try:
  624. # Take care of nested substitution references:
  625. for nested_ref in subdef_copy.findall(
  626. nodes.substitution_reference):
  627. nested_name = normed[nested_ref['refname'].lower()]
  628. if nested_name in nested.setdefault(nested_name, []):
  629. raise CircularSubstitutionDefinitionError
  630. nested[nested_name].append(key)
  631. nested_ref['ref-origin'] = ref
  632. subreflist.append(nested_ref)
  633. except CircularSubstitutionDefinitionError:
  634. parent = ref.parent
  635. if isinstance(parent, nodes.substitution_definition):
  636. msg = self.document.reporter.error(
  637. 'Circular substitution definition detected:',
  638. nodes.literal_block(parent.rawsource,
  639. parent.rawsource),
  640. line=parent.line, base_node=parent)
  641. parent.replace_self(msg)
  642. else:
  643. # find original ref substitution which caused this error
  644. ref_origin = ref
  645. while ref_origin.hasattr('ref-origin'):
  646. ref_origin = ref_origin['ref-origin']
  647. msg = self.document.reporter.error(
  648. 'Circular substitution definition referenced: '
  649. '"%s".' % refname, base_node=ref_origin)
  650. msgid = self.document.set_id(msg)
  651. prb = nodes.problematic(
  652. ref.rawsource, ref.rawsource, refid=msgid)
  653. prbid = self.document.set_id(prb)
  654. msg.add_backref(prbid)
  655. ref.replace_self(prb)
  656. continue
  657. ref.replace_self(subdef_copy.children)
  658. # register refname of the replacement node(s)
  659. # (needed for resolution of references)
  660. for node in subdef_copy.children:
  661. if isinstance(node, nodes.Referential):
  662. # HACK: verify refname attribute exists.
  663. # Test with docs/dev/todo.txt, see. |donate|
  664. if 'refname' in node:
  665. self.document.note_refname(node)
  666. class TargetNotes(Transform):
  667. """
  668. Creates a footnote for each external target in the text, and corresponding
  669. footnote references after each reference.
  670. """
  671. default_priority = 540
  672. """The TargetNotes transform has to be applied after `IndirectHyperlinks`
  673. but before `Footnotes`."""
  674. def __init__(self, document, startnode):
  675. Transform.__init__(self, document, startnode=startnode)
  676. self.classes = startnode.details.get('class', [])
  677. def apply(self):
  678. notes = {}
  679. nodelist = []
  680. for target in self.document.findall(nodes.target):
  681. # Only external targets.
  682. if not target.hasattr('refuri'):
  683. continue
  684. names = target['names']
  685. refs = []
  686. for name in names:
  687. refs.extend(self.document.refnames.get(name, []))
  688. if not refs:
  689. continue
  690. footnote = self.make_target_footnote(target['refuri'], refs,
  691. notes)
  692. if target['refuri'] not in notes:
  693. notes[target['refuri']] = footnote
  694. nodelist.append(footnote)
  695. # Take care of anonymous references.
  696. for ref in self.document.findall(nodes.reference):
  697. if not ref.get('anonymous'):
  698. continue
  699. if ref.hasattr('refuri'):
  700. footnote = self.make_target_footnote(ref['refuri'], [ref],
  701. notes)
  702. if ref['refuri'] not in notes:
  703. notes[ref['refuri']] = footnote
  704. nodelist.append(footnote)
  705. self.startnode.replace_self(nodelist)
  706. def make_target_footnote(self, refuri, refs, notes):
  707. if refuri in notes: # duplicate?
  708. footnote = notes[refuri]
  709. assert len(footnote['names']) == 1
  710. footnote_name = footnote['names'][0]
  711. else: # original
  712. footnote = nodes.footnote()
  713. footnote_id = self.document.set_id(footnote)
  714. # Use uppercase letters and a colon; they can't be
  715. # produced inside names by the parser.
  716. footnote_name = 'TARGET_NOTE: ' + footnote_id
  717. footnote['auto'] = 1
  718. footnote['names'] = [footnote_name]
  719. footnote_paragraph = nodes.paragraph()
  720. footnote_paragraph += nodes.reference('', refuri, refuri=refuri)
  721. footnote += footnote_paragraph
  722. self.document.note_autofootnote(footnote)
  723. self.document.note_explicit_target(footnote, footnote)
  724. for ref in refs:
  725. if isinstance(ref, nodes.target):
  726. continue
  727. refnode = nodes.footnote_reference(refname=footnote_name, auto=1)
  728. refnode['classes'] += self.classes
  729. self.document.note_autofootnote_ref(refnode)
  730. self.document.note_footnote_ref(refnode)
  731. index = ref.parent.index(ref) + 1
  732. reflist = [refnode]
  733. if not utils.get_trim_footnote_ref_space(self.document.settings):
  734. if self.classes:
  735. reflist.insert(
  736. 0, nodes.inline(text=' ', Classes=self.classes))
  737. else:
  738. reflist.insert(0, nodes.Text(' '))
  739. ref.parent.insert(index, reflist)
  740. return footnote
  741. class DanglingReferences(Transform):
  742. """
  743. Check for dangling references (incl. footnote & citation) and for
  744. unreferenced targets.
  745. """
  746. default_priority = 850
  747. def apply(self):
  748. visitor = DanglingReferencesVisitor(
  749. self.document,
  750. self.document.transformer.unknown_reference_resolvers)
  751. self.document.walk(visitor)
  752. # *After* resolving all references, check for unreferenced
  753. # targets:
  754. for target in self.document.findall(nodes.target):
  755. if not target.referenced:
  756. if target.get('anonymous'):
  757. # If we have unreferenced anonymous targets, there
  758. # is already an error message about anonymous
  759. # hyperlink mismatch; no need to generate another
  760. # message.
  761. continue
  762. if target['names']:
  763. naming = target['names'][0]
  764. elif target['ids']:
  765. naming = target['ids'][0]
  766. else:
  767. # Hack: Propagated targets always have their refid
  768. # attribute set.
  769. naming = target['refid']
  770. self.document.reporter.info(
  771. 'Hyperlink target "%s" is not referenced.'
  772. % naming, base_node=target)
  773. class DanglingReferencesVisitor(nodes.SparseNodeVisitor):
  774. def __init__(self, document, unknown_reference_resolvers):
  775. nodes.SparseNodeVisitor.__init__(self, document)
  776. self.document = document
  777. self.unknown_reference_resolvers = unknown_reference_resolvers
  778. def unknown_visit(self, node):
  779. pass
  780. def visit_reference(self, node):
  781. if node.resolved or not node.hasattr('refname'):
  782. return
  783. refname = node['refname']
  784. id = self.document.nameids.get(refname)
  785. if id is None:
  786. for resolver_function in self.unknown_reference_resolvers:
  787. if resolver_function(node):
  788. break
  789. else:
  790. if refname in self.document.nameids:
  791. msg = self.document.reporter.error(
  792. 'Duplicate target name, cannot be used as a unique '
  793. 'reference: "%s".' % (node['refname']), base_node=node)
  794. else:
  795. msg = self.document.reporter.error(
  796. 'Unknown target name: "%s".' % (node['refname']),
  797. base_node=node)
  798. msgid = self.document.set_id(msg)
  799. prb = nodes.problematic(
  800. node.rawsource, node.rawsource, refid=msgid)
  801. try:
  802. prbid = node['ids'][0]
  803. except IndexError:
  804. prbid = self.document.set_id(prb)
  805. msg.add_backref(prbid)
  806. node.replace_self(prb)
  807. else:
  808. del node['refname']
  809. node['refid'] = id
  810. self.document.ids[id].note_referenced_by(id=id)
  811. node.resolved = 1
  812. visit_footnote_reference = visit_citation_reference = visit_reference