@@ -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 #
0 commit comments