Skip to content

Commit

Permalink
Merge pull request #866 from sphinx-contrib/improve-anchor-links
Browse files Browse the repository at this point in the history
Improve anchor links
  • Loading branch information
jdknight authored Nov 19, 2023
2 parents c056035 + f939d1f commit fff543c
Show file tree
Hide file tree
Showing 14 changed files with 523 additions and 106 deletions.
10 changes: 3 additions & 7 deletions sphinxcontrib/confluencebuilder/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -321,16 +321,12 @@ def prepare_writing(self, docnames):
self.state.register_toctree_depth(
docname, toctree.get('maxdepth'))

# register title targets for references; however, not for v2
# editor since internal page links do not support linking
# directly to headers, so we will need to still generate anchors
# for these headers
if self.config.confluence_editor != 'v2':
self._register_doctree_title_targets(docname, doctree)

# post-prepare a ready doctree
self._prepare_doctree_writing(docname, doctree)

# register title targets for references
self._register_doctree_title_targets(docname, doctree)

# register titles for special documents (if needed); if a title is not
# already set from a placeholder document, configure a default title
if self.use_index and not self.state.title('genindex'):
Expand Down
187 changes: 88 additions & 99 deletions sphinxcontrib/confluencebuilder/storage/translator.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,9 +175,7 @@ def visit_start_of_file(self, node):
doc_target = self.state.target(doc_anchorname)
if not doc_target:
doc_id = node['docname']
self.body.append(self._start_ac_macro(node, 'anchor'))
self.body.append(self._build_ac_param(node, '', doc_id))
self.body.append(self._end_ac_macro(node, suffix=''))
self._build_anchor(node, doc_id)

def pre_body_data(self):
data = ''
Expand Down Expand Up @@ -215,21 +213,30 @@ def visit_title(self, node):
self.body.append(
self._start_tag(node, f'h{self._title_level}'))

# generate anchors inside headers for v2, to avoid extra
# spacing from an anchor macro
if self.v2:
# hinted to build an anchor for a target
anchor = node.parent.get('embedded-anchor', None)
if anchor:
self.body.append(self._start_ac_macro(node, 'anchor'))
self.body.append(self._build_ac_param(node, '', anchor))
self.body.append(self._end_ac_macro(node, suffix=''))
# build an anchor if content references this title
elif 'refid' in node:
target = '-'.join(node.astext().split()).lower()
self.body.append(self._start_ac_macro(node, 'anchor'))
self.body.append(self._build_ac_param(node, '', target))
self.body.append(self._end_ac_macro(node, suffix=''))
# For v2, will will generate section anchors inside the title
# area for the following reasons:
# - We want to create inside the header inside if we input anchors
# before the header, it increase the space above the anchor
# due to how v2 styles a page.
# - We are generating compatible anchor links (prefixed with the
# repsective document name) which helps allow `ac:link` macros
# properly link when coming from v1 or v2 editor pages.
if self.v2 and 'names' in node.parent:
for anchor in node.parent['names']:
target_name = f'{self.docname}#{anchor}'
target = self.state.target(target_name)
if target:
self._build_anchor(node, target)

# For MyST sections with an auto-generated slug, we will use this
# slug to build an anchor target for anchor links defined in a
# Markdown page.
slug = node.parent.get('slug')
if slug:
target_name = f'{self.docname}#{slug}'
target = self.state.target(target_name)
if target:
self._build_anchor(node, target)

self.add_secnumber(node)
self.add_fignumber(node.parent)
Expand All @@ -239,19 +246,10 @@ def visit_title(self, node):
# reference, create a link to it
if 'refid' in node and not node.next_node(nodes.reference):
anchor_value = ''.join(node['refid'].split())

if self.v2:
attribs = {
'href': f'#{anchor_value}',
}

self.body.append(self._start_tag(node, 'a', **attribs))
self.context.append(self._end_tag(node, suffix=''))
else:
self.body.append(self._start_ac_link(node, anchor_value))
self.body.append(self._start_ac_link_body(node))
self.context.append(self._end_ac_link_body(node) +
self._end_ac_link(node))
self.body.append(self._start_ac_link(node, anchor_value))
self.body.append(self._start_ac_link_body(node))
self.context.append(self._end_ac_link_body(node) +
self._end_ac_link(node))
elif (isinstance(node.parent, addnodes.compact_paragraph) and
node.parent.get('toctree')):
self.visit_caption(node)
Expand Down Expand Up @@ -298,14 +296,15 @@ def visit_paragraph(self, node):

self.body.append(self._start_tag(node, 'p', **attribs))

# generate anchors inside paragraphs for v2, to avoid extra
# spacing from an anchor macro
if self.v2:
anchor = node.get('embedded-anchor', None)
if anchor:
self.body.append(self._start_ac_macro(node, 'anchor'))
self.body.append(self._build_ac_param(node, '', anchor))
self.body.append(self._end_ac_macro(node, suffix=''))
# For any names assigned to a paragraph, generate an anchor link to
# ensure content can jump to this specific paragraph. This was
# originally handled in `visit_target`, but now applied here since in
# v2, anchors need to be inside paragraphs to prevent any undesired
# extra spacing above the paragraph (before or after for v1, there is
# no difference).
if 'names' in node:
for anchor in node['names']:
self._build_anchor(node, anchor)

self.context.append(self._end_tag(node, suffix=''))

Expand Down Expand Up @@ -521,9 +520,7 @@ def visit_term(self, node):

if 'ids' in node:
for id_ in node['ids']:
self.body.append(self._start_ac_macro(node, 'anchor'))
self.body.append(self._build_ac_param(node, '', id_))
self.body.append(self._end_ac_macro(node, suffix=''))
self._build_anchor(node, id_)

if not self.v2:
self.body.append(self._start_tag(node, 'dt'))
Expand Down Expand Up @@ -994,9 +991,7 @@ def _visit_todo_node(self, node):
self.body.append(self._start_tag(node, 'h3'))

if 'ids' in node and node['ids']:
self.body.append(self._start_ac_macro(node, 'anchor'))
self.body.append(self._build_ac_param(node, '', node['ids'][0]))
self.body.append(self._end_ac_macro(node, suffix=''))
self._build_anchor(node, node['ids'][0])

if self.v2:
self.body.append(SL('Todo'))
Expand Down Expand Up @@ -1319,9 +1314,7 @@ def _visit_reference_intern_id(self, node):

if anchor_value and (is_citation or self._topic) and 'ids' in node:
for id_ in node['ids']:
self.body.append(self._start_ac_macro(node, 'anchor'))
self.body.append(self._build_ac_param(node, '', id_))
self.body.append(self._end_ac_macro(node, suffix=''))
self._build_anchor(node, id_)

if is_citation:
self.body.append(self._start_tag(node, 'sup'))
Expand Down Expand Up @@ -1435,32 +1428,13 @@ def depart_reference(self, node):
self._reference_context = []

def visit_target(self, node):
if 'refid' in node:
anchor = ''.join(node['refid'].split())

# for v2 editor, we will flag a section/paragraph node to build
# an anchor for use (to prevent an undesired newline) inside a
# heading or before a paragraph
if self.v2:
next_sibling = first(findall(node,
include_self=False, descend=False, siblings=True))
if next_sibling:
next_sibling['embedded-anchor'] = anchor
raise nodes.SkipNode

# only build an anchor if required (e.g. is a reference label
# already provided by a build section element)
target_name = f'{self.docname}#{anchor}'
target = self.state.target(target_name)
if not target:
self.body.append(self._start_ac_macro(node, 'anchor'))
self.body.append(self._build_ac_param(node, '', anchor))
self.body.append(self._end_ac_macro(node, suffix=''))
elif 'ids' in node and 'refuri' not in node:
# for any target identifiers that do not have a reference uri (e.g.
# sections which will have automatically created targets), we will
# build an anchor link for them; example cases include documentation
# which generate a custom anchor link inside a paragraph
if 'ids' in node and 'refuri' not in node:
for id_ in node['ids']:
self.body.append(self._start_ac_macro(node, 'anchor'))
self.body.append(self._build_ac_param(node, '', id_))
self.body.append(self._end_ac_macro(node, suffix=''))
self._build_anchor(node, id_)

self.body.append(self.encode(node.astext()))

Expand Down Expand Up @@ -1502,9 +1476,7 @@ def visit_footnote(self, node):
**{'style': 'border: none'}))

# footnote anchor
self.body.append(self._start_ac_macro(node, 'anchor'))
self.body.append(self._build_ac_param(node, '', node['ids'][0]))
self.body.append(self._end_ac_macro(node, suffix=''))
self._build_anchor(node, node['ids'][0])

# footnote label and back reference(s)
if 'backrefs' not in node or not node['backrefs']:
Expand Down Expand Up @@ -1588,28 +1560,17 @@ def visit_footnote_reference(self, node):
text = f"[{node.astext()}]"

# build an anchor for back reference
self.body.append(self._start_ac_macro(node, 'anchor'))
self.body.append(self._build_ac_param(node, '', node['ids'][0]))
self.body.append(self._end_ac_macro(node, suffix=''))
self._build_anchor(node, node['ids'][0])

# link to anchor
target_anchor = ''.join(node['refid'].split())

self.body.append(self._start_tag(node, 'sup'))
if self.v2:
attribs = {
'href': f'#{target_anchor}',
}

self.body.append(self._start_tag(node, 'a', **attribs))
self.body.append(self._escape_cdata(text))
self.body.append(self._end_tag(node, suffix=''))
else:
self.body.append(self._start_ac_link(node, target_anchor))
self.body.append(self._start_ac_plain_text_link_body_macro(node))
self.body.append(self._escape_cdata(text))
self.body.append(self._end_ac_plain_text_link_body_macro(node))
self.body.append(self._end_ac_link(node))
self.body.append(self._start_ac_link(node, target_anchor))
self.body.append(self._start_ac_plain_text_link_body_macro(node))
self.body.append(self._escape_cdata(text))
self.body.append(self._end_ac_plain_text_link_body_macro(node))
self.body.append(self._end_ac_link(node))
self.body.append(self._end_tag(node, suffix='')) # sup
raise nodes.SkipNode

Expand Down Expand Up @@ -1846,9 +1807,7 @@ def _visit_image(self, node, opts):
self.body.append(self._start_tag(node, 'ac:layout-cell'))
if 'ids' in node:
for id_ in node['ids']:
self.body.append(self._start_ac_macro(node, 'anchor'))
self.body.append(self._build_ac_param(node, '', id_))
self.body.append(self._end_ac_macro(node, suffix=''))
self._build_anchor(node, id_)
self.body.append(self._end_tag(node))

self.body.append(self._start_tag(node, 'ac:layout-cell'))
Expand Down Expand Up @@ -2163,9 +2122,7 @@ def depart_desc_signature(self, node):
def visit_desc_signature_line(self, node):
if self._desc_sig_ids:
for id_ in self._desc_sig_ids:
self.body.append(self._start_ac_macro(node, 'anchor'))
self.body.append(self._build_ac_param(node, '', id_))
self.body.append(self._end_ac_macro(node, suffix=''))
self._build_anchor(node, id_)

if self._desc_sig_ids is None:
self.body.append(self._start_tag(
Expand Down Expand Up @@ -3130,6 +3087,38 @@ def visit_raw(self, node):
# # #
# ##########################################################################

def _build_anchor(self, node, anchor):
"""
build an anchor on a page
A helper is used to build a anchor on a current page. Using the
provided anchor value, an anchor macro will be added to the body of
the page.
In addition, for v2 editor, an additional macro will be created to
emulate the original Confluence design of document-prefixed anchors.
While we would like to move away from this format, ac:links to other
pages with anchors requires the prefixes to properly link (since they
expect the prefix to exist; see `_visit_reference_intern_uri`).
Args:
node: the node adding the anchor
anchor: the name of the anchor to create
"""

self.body.append(self._start_ac_macro(node, 'anchor'))
self.body.append(self._build_ac_param(node, '', anchor))
self.body.append(self._end_ac_macro(node, suffix=''))

if self.v2:
doctitle = self.state.title(self.docname)
doctitle = self.encode(doctitle.replace(' ', ''))

compat_anchor = f'{doctitle}-{anchor}'
self.body.append(self._start_ac_macro(node, 'anchor'))
self.body.append(self._build_ac_param(node, '', compat_anchor))
self.body.append(self._end_ac_macro(node, suffix=''))

def _start_tag(self, node, tag, suffix=None, empty=False, **kwargs):
"""
generates start tag content for a given node
Expand Down
10 changes: 10 additions & 0 deletions tests/sample-sets/header-links/conf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
extensions = [
'myst_parser',
'sphinxcontrib.confluencebuilder',
]

myst_enable_extensions = [
'colon_fence',
]

myst_heading_anchors = 7
14 changes: 14 additions & 0 deletions tests/sample-sets/header-links/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
Index
=====

.. toctree::
:maxdepth: 1

rst-v1-first
rst-v1-second
rst-v2-first
rst-v2-second
md-v1-first
md-v1-second
md-v2-first
md-v2-second
19 changes: 19 additions & 0 deletions tests/sample-sets/header-links/md-v1-first.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
:::{confluence_metadata}
:editor: v1
:::

# Markdown v1 First

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris egestas enim ex, quis rhoncus erat vulputate luctus. Donec neque leo, sodales vitae orci sit amet, consectetur congue ipsum. Morbi egestas fermentum nibh pulvinar egestas. Nunc bibendum lobortis orci et tempor. In rhoncus libero ornare est aliquam, id dictum urna ultricies. Donec pretium eros nunc, sit amet ultrices justo blandit in. Aenean vitae blandit ligula. Morbi augue odio, placerat in mi vitae, luctus hendrerit risus. Aliquam in neque mauris. Quisque porta felis nunc, ac porta elit aliquet vitae. Fusce fringilla, ex ac tincidunt pharetra, ligula tellus facilisis purus, semper blandit lectus orci sit amet sapien. Integer sagittis enim purus, a ornare felis malesuada eu. Aenean vel dui bibendum, ornare risus et, commodo velit.

Cras ullamcorper, dui id feugiat sagittis, magna mauris volutpat mi, quis pulvinar ante urna a nisi. Maecenas quam nisl, tempor eu aliquam in, lacinia ut nisi. Maecenas ut sem eu lorem convallis consectetur. Proin ornare purus sit amet aliquam maximus. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Duis condimentum fermentum aliquam. Aenean non tempor purus. Aliquam erat volutpat. Ut ac quam at velit viverra hendrerit. Curabitur commodo arcu sed nisi ultrices, eget mollis sapien pellentesque. Vestibulum venenatis, mauris at aliquet malesuada, tortor arcu ultricies lorem, non mollis nisi erat sed elit. Suspendisse potenti. Cras vitae dolor vel sem porta congue. Donec vehicula eros ut magna finibus semper. Phasellus a consequat massa.

Nam placerat faucibus tellus, a volutpat nibh imperdiet at. Aenean egestas, tellus at commodo mollis, nibh lacus porta tellus, eget commodo erat massa ac orci. Proin at eros eu velit gravida aliquam. Phasellus vel feugiat augue. Proin tristique iaculis lobortis. Integer id bibendum ante. Duis lacinia sodales libero, at euismod quam eleifend id. Vestibulum laoreet dolor vitae scelerisque dignissim.

## Markdown v1 First sub-heading

Aenean rhoncus magna metus. Aenean malesuada leo sed orci ultrices pulvinar in ut eros. Etiam a fringilla arcu, vitae vehicula eros. Suspendisse potenti. Proin dignissim risus ut vulputate imperdiet. Praesent quis placerat velit, eu pellentesque ante. Integer sed pretium risus, at hendrerit urna. Proin consequat gravida nunc vitae euismod. Curabitur ut massa at nulla molestie auctor. Phasellus euismod euismod dictum. Nulla volutpat purus eu sapien maximus, nec euismod tortor finibus.

Maecenas posuere nunc nec maximus scelerisque. Cras viverra scelerisque pulvinar. Phasellus posuere tristique dui eget rhoncus. Fusce ultricies posuere pharetra. Quisque finibus diam dolor, in consequat nisl pellentesque in. Morbi sodales nulla ut tempor congue. Nulla congue vehicula eros eu pellentesque. In sollicitudin ullamcorper lacinia. Pellentesque suscipit ante in sollicitudin pulvinar. Nulla tellus nibh, molestie nec massa nec, varius sodales ex.

Nunc eget enim a nisi pretium imperdiet. Proin auctor euismod porttitor. Sed molestie imperdiet erat. Nam ut nisl porttitor nibh scelerisque vehicula. Proin pharetra, dui nec finibus posuere, nunc ex fringilla massa, sit amet interdum purus purus vitae sem. Morbi vitae enim gravida, aliquam eros nec, malesuada eros. Proin tristique sodales fermentum. Donec tristique condimentum nulla a pretium. Cras rutrum feugiat venenatis. Ut interdum vitae justo in rutrum. Etiam rutrum eleifend nulla ullamcorper consequat.
Loading

0 comments on commit fff543c

Please sign in to comment.