Skip to content

Commit d4f0861

Browse files
authored
Merge pull request #1125 from sphinx-contrib/delayed-anchor-injection
translator: delayed anchor injection for other refids
2 parents 40d54a6 + 5bd97a4 commit d4f0861

File tree

3 files changed

+140
-48
lines changed

3 files changed

+140
-48
lines changed

sphinxcontrib/confluencebuilder/storage/translator.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,13 +110,15 @@ def __init__(self, document, builder):
110110
self.numfig_format = config.numfig_format
111111
self.secnumber_suffix = config.confluence_secnumber_suffix
112112
self.todo_include_todos = getattr(config, 'todo_include_todos', None)
113+
self._anchor_cache = set()
113114
self._auto_context = []
114115
self._building_footnotes = False
115116
self._figure_context = []
116117
self._indent_level = 0
117118
self._list_context = ['']
118119
self._manpage_url = getattr(config, 'manpages_url', None)
119120
self._needs_navnode_spacing = False
121+
self._pending_anchors = []
120122
self._reference_context = []
121123
self._thead_context = []
122124
self._v2_header_added = False
@@ -267,6 +269,7 @@ def visit_title(self, node):
267269
new_targets = []
268270

269271
self.body.append(self.start_tag(node, f'h{self._title_level}'))
272+
self._delayed_anchor_inject(node)
270273

271274
if self.builder.name == 'singleconfluence':
272275
docname = self._docnames[-1]
@@ -1502,9 +1505,14 @@ def visit_target(self, node):
15021505
# sections which will have automatically created targets), we will
15031506
# build an anchor link for them; example cases include documentation
15041507
# which generate a custom anchor link inside a paragraph
1505-
if 'ids' in node and 'refuri' not in node:
1508+
if node.get('ids') and 'refuri' not in node:
15061509
self._build_id_anchors(node)
15071510
self.body.append(self.encode(node.astext()))
1511+
# if this target has a reference id, treat as a generic anchor
1512+
elif node.get('refid'):
1513+
# note: we do not add generic anchors immediately to avoid
1514+
# newline issues
1515+
self._pending_anchors.append(node.get('refid'))
15081516

15091517
raise nodes.SkipNode
15101518

@@ -3430,6 +3438,7 @@ def visit_line_block(self, node):
34303438
attribs['style'] = style
34313439

34323440
self.body.append(self.start_tag(node, tag, **attribs))
3441+
self._delayed_anchor_inject(node)
34333442
self.context.append(self.end_tag(node))
34343443

34353444
def depart_line_block(self, node):
@@ -3498,6 +3507,9 @@ def _build_id_anchors(self, node):
34983507
for id_ in node['ids']:
34993508
self._build_anchor(node, id_)
35003509

3510+
# also, include any pending anchors not added
3511+
self._delayed_anchor_inject(node)
3512+
35013513
def _build_anchor(self, node, anchor, *, force_compat=False):
35023514
"""
35033515
build an anchor on a page
@@ -3518,6 +3530,12 @@ def _build_anchor(self, node, anchor, *, force_compat=False):
35183530
force_compat (optional): always force compat anchor
35193531
"""
35203532

3533+
# ignore any duplicate anchors on the same page
3534+
if anchor in self._anchor_cache:
3535+
self.verbose(f'duplicate anchor ({self.docname}): {anchor}')
3536+
return
3537+
self._anchor_cache.add(anchor)
3538+
35213539
self.verbose(f'build anchor ({self.docname}): {anchor}')
35223540
self.body.append(self.start_ac_macro(node, 'anchor'))
35233541
self.body.append(self.build_ac_param(node, '', anchor))
@@ -4118,6 +4136,24 @@ def escape_cdata(self, data):
41184136

41194137
return ConfluenceBaseTranslator.encode(self, data)
41204138

4139+
def _delayed_anchor_inject(self, node):
4140+
"""
4141+
add delayed anchors into a node
4142+
4143+
While it would be nice to add anchors when processing targets
4144+
immediately, Confluence renders anchors in a way where they can result
4145+
in extra newlines. A trick that appears to work is for some anchors,
4146+
we can delay adding them into the body until we build a block (e.g. a
4147+
paragraph) to avoid any extra spacing.
4148+
4149+
Args:
4150+
node: the node to add the anchor into
4151+
"""
4152+
4153+
while self._pending_anchors:
4154+
new_anchor_id = self._pending_anchors.pop()
4155+
self._build_anchor(node, new_anchor_id)
4156+
41214157
# ##########################################################################
41224158
# # #
41234159
# # deprecated helpers #

tests/unit-tests/test_references_confluence.py

Lines changed: 63 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -86,21 +86,25 @@ def test_storage_references_confluence(self):
8686

8787
# anchor in local-toc
8888
ltoc_entry = local_toc_entries.pop(0)
89-
ltoc_anchor = ltoc_entry.find(
89+
ltoc_anchors = ltoc_entry.find_all(
9090
'ac:structured-macro', {'ac:name': 'anchor'})
91-
self.assertIsNotNone(ltoc_anchor)
92-
anchor_param = ltoc_anchor.find('ac:parameter')
93-
self.assertIsNotNone(anchor_param)
94-
anchor_01_id = anchor_param.text
91+
self.assertGreaterEqual(len(ltoc_anchors), 1)
92+
anchor_01_ids = []
93+
for ltoc_anchor in ltoc_anchors:
94+
anchor_param = ltoc_anchor.find('ac:parameter')
95+
self.assertIsNotNone(anchor_param)
96+
anchor_01_ids.append(anchor_param.text)
9597

9698
# anchor in local-toc
9799
ltoc_entry = local_toc_entries.pop(0)
98-
ltoc_anchor = ltoc_entry.find(
100+
ltoc_anchors = ltoc_entry.find_all(
99101
'ac:structured-macro', {'ac:name': 'anchor'})
100-
self.assertIsNotNone(ltoc_anchor)
101-
anchor_param = ltoc_anchor.find('ac:parameter')
102-
self.assertIsNotNone(anchor_param)
103-
anchor_02_id = anchor_param.text
102+
self.assertGreaterEqual(len(ltoc_anchors), 1)
103+
anchor_02_ids = []
104+
for ltoc_anchor in ltoc_anchors:
105+
anchor_param = ltoc_anchor.find('ac:parameter')
106+
self.assertIsNotNone(anchor_param)
107+
anchor_02_ids.append(anchor_param.text)
104108

105109
# anchor after pre-content
106110
content_element = data.find('p', text='pre-content')
@@ -138,15 +142,15 @@ def test_storage_references_confluence(self):
138142
# header link to a local-toc entry
139143
ac_link = ac_links.pop(0)
140144
self.assertTrue(ac_link.has_attr('ac:anchor'))
141-
self.assertEqual(ac_link['ac:anchor'], anchor_01_id)
145+
self.assertIn(ac_link['ac:anchor'], anchor_01_ids)
142146
ac_link_body = ac_link.find('ac:link-body')
143147
self.assertIsNotNone(ac_link_body)
144148
self.assertEqual(ac_link_body.text, 'An Extra Header')
145149

146150
# header link to a local-toc entry
147151
ac_link = ac_links.pop(0)
148152
self.assertTrue(ac_link.has_attr('ac:anchor'))
149-
self.assertEqual(ac_link['ac:anchor'], anchor_02_id)
153+
self.assertIn(ac_link['ac:anchor'], anchor_02_ids)
150154
ac_link_body = ac_link.find('ac:link-body')
151155
self.assertIsNotNone(ac_link_body)
152156
self.assertEqual(ac_link_body.text, 'An Extra Header')
@@ -295,21 +299,25 @@ def test_storage_references_confluence(self):
295299

296300
# anchor in local-toc
297301
ltoc_entry = local_toc_entries.pop(0)
298-
ltoc_anchor = ltoc_entry.find(
302+
ltoc_anchors = ltoc_entry.find_all(
299303
'ac:structured-macro', {'ac:name': 'anchor'})
300-
self.assertIsNotNone(ltoc_anchor)
301-
anchor_param = ltoc_anchor.find('ac:parameter')
302-
self.assertIsNotNone(anchor_param)
303-
anchor_01_id = anchor_param.text
304+
self.assertGreaterEqual(len(ltoc_anchors), 1)
305+
anchor_01_ids = []
306+
for ltoc_anchor in ltoc_anchors:
307+
anchor_param = ltoc_anchor.find('ac:parameter')
308+
self.assertIsNotNone(anchor_param)
309+
anchor_01_ids.append(anchor_param.text)
304310

305311
# anchor in local-toc
306312
ltoc_entry = local_toc_entries.pop(0)
307-
ltoc_anchor = ltoc_entry.find(
313+
ltoc_anchors = ltoc_entry.find_all(
308314
'ac:structured-macro', {'ac:name': 'anchor'})
309-
self.assertIsNotNone(ltoc_anchor)
310-
anchor_param = ltoc_anchor.find('ac:parameter')
311-
self.assertIsNotNone(anchor_param)
312-
anchor_02_id = anchor_param.text
315+
self.assertGreaterEqual(len(ltoc_anchors), 1)
316+
anchor_02_ids = []
317+
for ltoc_anchor in ltoc_anchors:
318+
anchor_param = ltoc_anchor.find('ac:parameter')
319+
self.assertIsNotNone(anchor_param)
320+
anchor_02_ids.append(anchor_param.text)
313321

314322
# anchor after pre-content
315323
content_element = data.find('p', text='pre-content')
@@ -331,15 +339,15 @@ def test_storage_references_confluence(self):
331339
# header link to a local-toc entry
332340
ac_link = ac_links.pop(0)
333341
self.assertTrue(ac_link.has_attr('ac:anchor'))
334-
self.assertEqual(ac_link['ac:anchor'], anchor_01_id)
342+
self.assertIn(ac_link['ac:anchor'], anchor_01_ids)
335343
ac_link_body = ac_link.find('ac:link-body')
336344
self.assertIsNotNone(ac_link_body)
337345
self.assertEqual(ac_link_body.text, 'An Extra Header')
338346

339347
# header link to a local-toc entry
340348
ac_link = ac_links.pop(0)
341349
self.assertTrue(ac_link.has_attr('ac:anchor'))
342-
self.assertEqual(ac_link['ac:anchor'], anchor_02_id)
350+
self.assertIn(ac_link['ac:anchor'], anchor_02_ids)
343351
ac_link_body = ac_link.find('ac:link-body')
344352
self.assertIsNotNone(ac_link_body)
345353
self.assertEqual(ac_link_body.text, 'An Extra Header')
@@ -497,21 +505,25 @@ def test_storage_references_confluence(self):
497505

498506
# anchor in first header
499507
header_entry = header_entries.pop(0)
500-
header_anchor = header_entry.find(
508+
header_anchors = header_entry.find_all(
501509
'ac:structured-macro', {'ac:name': 'anchor'})
502-
self.assertIsNotNone(header_anchor)
503-
anchor_param = header_anchor.find('ac:parameter')
504-
self.assertIsNotNone(anchor_param)
505-
anchor_01_id = anchor_param.text
510+
self.assertGreaterEqual(len(ltoc_anchors), 1)
511+
anchor_01_ids = []
512+
for header_anchor in header_anchors:
513+
anchor_param = header_anchor.find('ac:parameter')
514+
self.assertIsNotNone(anchor_param)
515+
anchor_01_ids.append(anchor_param.text)
506516

507517
# anchor in second header
508518
header_entry = header_entries.pop(0)
509-
header_anchor = header_entry.find(
519+
header_anchors = header_entry.find_all(
510520
'ac:structured-macro', {'ac:name': 'anchor'})
511-
self.assertIsNotNone(header_anchor)
512-
anchor_param = header_anchor.find('ac:parameter')
513-
self.assertIsNotNone(anchor_param)
514-
anchor_02_id = anchor_param.text
521+
self.assertGreaterEqual(len(ltoc_anchors), 1)
522+
anchor_02_ids = []
523+
for header_anchor in header_anchors:
524+
anchor_param = header_anchor.find('ac:parameter')
525+
self.assertIsNotNone(anchor_param)
526+
anchor_02_ids.append(anchor_param.text)
515527

516528
# ##########################################################
517529
# find the expected ac:link macros
@@ -522,7 +534,7 @@ def test_storage_references_confluence(self):
522534
# heading jump to other heading
523535
ac_link = ac_links.pop(0)
524536
self.assertTrue(ac_link.has_attr('ac:anchor'))
525-
self.assertEqual(ac_link['ac:anchor'], anchor_01_id)
537+
self.assertIn(ac_link['ac:anchor'], anchor_01_ids)
526538
link_page = ac_link.find('ri:page')
527539
self.assertIsNone(link_page)
528540
ac_link_body = ac_link.find('ac:link-body')
@@ -532,7 +544,7 @@ def test_storage_references_confluence(self):
532544
# link to the sub-heading on this page
533545
ac_link = ac_links.pop(0)
534546
self.assertTrue(ac_link.has_attr('ac:anchor'))
535-
self.assertEqual(ac_link['ac:anchor'], anchor_01_id)
547+
self.assertIn(ac_link['ac:anchor'], anchor_01_ids)
536548
link_page = ac_link.find('ri:page')
537549
self.assertIsNone(link_page)
538550
ac_link_body = ac_link.find('ac:link-body')
@@ -543,7 +555,7 @@ def test_storage_references_confluence(self):
543555
# link to the second sub-heading on this page
544556
ac_link = ac_links.pop(0)
545557
self.assertTrue(ac_link.has_attr('ac:anchor'))
546-
self.assertEqual(ac_link['ac:anchor'], anchor_02_id)
558+
self.assertIn(ac_link['ac:anchor'], anchor_02_ids)
547559
link_page = ac_link.find('ri:page')
548560
self.assertIsNone(link_page)
549561
ac_link_body = ac_link.find('ac:link-body')
@@ -607,21 +619,25 @@ def test_storage_references_confluence(self):
607619

608620
# anchor in first header
609621
header_entry = header_entries.pop(0)
610-
header_anchor = header_entry.find(
622+
header_anchors = header_entry.find_all(
611623
'ac:structured-macro', {'ac:name': 'anchor'})
612-
self.assertIsNotNone(header_anchor)
613-
anchor_param = header_anchor.find('ac:parameter')
614-
self.assertIsNotNone(anchor_param)
615-
anchor_01_id = anchor_param.text
624+
self.assertGreaterEqual(len(ltoc_anchors), 1)
625+
anchor_01_ids = []
626+
for header_anchor in header_anchors:
627+
anchor_param = header_anchor.find('ac:parameter')
628+
self.assertIsNotNone(anchor_param)
629+
anchor_01_ids.append(anchor_param.text)
616630

617631
# anchor in second header
618632
header_entry = header_entries.pop(0)
619-
header_anchor = header_entry.find(
633+
header_anchors = header_entry.find_all(
620634
'ac:structured-macro', {'ac:name': 'anchor'})
621-
self.assertIsNotNone(header_anchor)
622-
anchor_param = header_anchor.find('ac:parameter')
623-
self.assertIsNotNone(anchor_param)
624-
anchor_02_id = anchor_param.text
635+
self.assertGreaterEqual(len(ltoc_anchors), 1)
636+
anchor_02_ids = []
637+
for header_anchor in header_anchors:
638+
anchor_param = header_anchor.find('ac:parameter')
639+
self.assertIsNotNone(anchor_param)
640+
anchor_02_ids.append(anchor_param.text)
625641

626642
# ##########################################################
627643
# find the expected ac:link macros

tests/validation-sets/restructuredtext/references.rst

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,46 @@ pharetra quis orci. Maecenas lobortis neque ipsum, quis ultricies velit auctor
6161
eu. Quisque pellentesque suscipit sodales. In sapien massa, tincidunt vel nisl
6262
id, sodales bibendum dui. Pellentesque consectetur a risus sed aliquam.
6363

64+
Reference in a list: `other values`_
65+
66+
#. Curabitur tincidunt eros non auctor commodo.
67+
#. Fusce vestibulum erat id massa vehicula, a suscipit ligula vestibulum.
68+
69+
.. _other values:
70+
71+
#. Fusce quis nibh quis dui aliquet maximus ac vel felis.
72+
#. Duis vehicula sem non turpis eleifend imperdiet.
73+
74+
Integer aliquet purus elementum leo pulvinar elementum. Aenean id orci cursus,
75+
viverra velit id, aliquam ipsum. Pellentesque nec magna ultricies, consequat
76+
metus a, sollicitudin libero. Aenean placerat, nibh at fermentum rutrum, metus
77+
nulla interdum mauris, sit amet sagittis diam elit vel elit. Vestibulum a
78+
mattis metus, et condimentum metus. Praesent pharetra, nisi ullamcorper
79+
tincidunt convallis, purus dolor placerat justo, at sodales elit nisl sed nisi.
80+
Nam bibendum tempor elit a fermentum. Vivamus tellus massa, vulputate a dolor
81+
eu, placerat pharetra justo. Pellentesque habitant morbi tristique senectus et
82+
netus et malesuada fames ac turpis egestas. Nunc fringilla nibh id dictum
83+
semper. Nam pharetra, tortor at porttitor sollicitudin, eros erat ultricies
84+
ante, nec cursus dui risus nec velit.
85+
86+
Cras cursus in magna non ultrices. In nulla arcu, malesuada a blandit id,
87+
pretium ac dui. Praesent porta sodales turpis, vitae efficitur libero finibus
88+
sit amet. Nulla vitae molestie leo. Suspendisse nisl sapien, pulvinar ut
89+
ultricies ac, placerat vel ipsum. Pellentesque in ex volutpat, tincidunt eros
90+
sed, tristique nisi. Suspendisse sit amet dui justo. Maecenas convallis ligula
91+
a vestibulum pulvinar. Phasellus magna magna, maximus eu elit lacinia,
92+
venenatis ultrices libero.
93+
94+
Mauris lobortis fringilla vestibulum. Donec quis sagittis erat. Mauris bibendum
95+
metus magna, sit amet pellentesque nibh mollis vitae. Maecenas interdum eget
96+
mi ac consectetur. Pellentesque fermentum gravida nisi, quis accumsan leo
97+
fringilla posuere. Proin sed luctus ipsum. Sed sit amet augue dui. Donec
98+
lobortis porta sapien, sit amet faucibus ipsum iaculis at. Nunc vel enim vel
99+
neque malesuada lacinia. Aliquam ante velit, interdum vitae purus eu, tristique
100+
placerat augue. Etiam ut consequat neque. Praesent tempor metus in lorem
101+
efficitur iaculis. Vivamus enim risus, molestie sed massa sit amet, iaculis
102+
facilisis velit. Aenean ornare tincidunt vestibulum. Nulla bibendum nisi in
103+
neque congue rutrum.
64104

65105
.. references ------------------------------------------------------------------
66106

0 commit comments

Comments
 (0)