Skip to content

Introduce inline tab support #1117

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions doc/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2238,6 +2238,58 @@ Advanced processing configuration

.. versionadded:: 2.2

.. _confluence_tab_macro:

.. confval:: confluence_tab_macro

.. attention::

This feature is considered experimental. Inlined tabs are supported
through third-party Sphinx extensions as well as only supported on
Confluence instances using third-party marketplace extensions. The
combination is less than ideal for providing consistent results and
performing testing. This may be used by advanced users who can take
advantage of their instance's configurations to utilize inlined tabs.

This configuration is used when attempting to build macros for rendering
inlined tabs on a page. It is used to define what third-party macro can
be used to render inlined tabs. This configuration also defines what
parameter values dictate the name of a tab and well as which tab is
considered to be the first/primary tab for a set.

.. code-block:: python

confluence_tab_macro = {
'macro-name': 'macro-name',
'primary-id': 'id-for-primary-parameter',
'primary-value': 'value-to-primary-parameter',
'title-id': 'id-for-title-parameter',
}

The following configurations are known to function for the listed
marketplace extensions:

.. list-table::
:header-rows: 1
:widths: 40 60

* - Marketplace Application

- Configuration

* - Mosaic: Content Formatting Macros & Templates for Confluence

- .. code-block:: python

confluence_tab_macro = {
'macro-name': 'cfm-tabs-page',
'primary-id': 'primaryTab',
'primary-value': 'true',
'title-id': 'tabsPageTitle',
}

.. versionadded:: 2.14

.. _confluence_remove_title:

.. confval:: confluence_remove_title
Expand Down
28 changes: 28 additions & 0 deletions doc/features.rst
Original file line number Diff line number Diff line change
Expand Up @@ -248,12 +248,38 @@ Type Notes
support for tabs, button and inline icons.
`sphinx-diagrams`_ Supported
`sphinx-gallery`_ Supported
`sphinx-inline-tabs`_ Limited support.

Requires a Confluence instance that supports
inlined tabs through the use of a third-party
macro. :lref:`confluence_tab_macro` must be
configured.
`sphinx-needs`_ Limited support.

Formatting of content may not be as expected.
The ``needs_default_layout`` option may need
to be tailored specifically for a Confluence
build.
`sphinx-tabs`_ Limited support.

Requires a Confluence instance that supports
inlined tabs through the use of a third-party
macro. :lref:`confluence_tab_macro` must be
configured.

Features such as group tabs are not
supported.

May require an explicit registration for
support with sphinx-tabs:

.. code-block:: python

sphinx_tabs_valid_builders = [
'confluence',
'singleconfluence',
]

`sphinx-toolbox`_ Supported
`sphinxcontrib-aafig`_ Supported.

Expand Down Expand Up @@ -354,7 +380,9 @@ has another concern, feel free to bring up an issue:
.. _sphinx-design: https://sphinx-design.readthedocs.io/
.. _sphinx-diagrams: https://pypi.org/project/sphinx-diagrams/
.. _sphinx-gallery: https://sphinx-gallery.github.io/
.. _sphinx-inline-tabs: https://sphinx-inline-tabs.readthedocs.io/
.. _sphinx-needs: https://sphinxcontrib-needs.readthedocs.io/
.. _sphinx-tabs: https://sphinx-tabs.readthedocs.io/
.. _sphinx-toolbox: https://sphinx-toolbox.readthedocs.io/
.. _sphinx.ext.autodoc: https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html
.. _sphinx.ext.autosectionlabel: https://www.sphinx-doc.org/en/master/usage/extensions/autosectionlabel.html
Expand Down
2 changes: 2 additions & 0 deletions sphinxcontrib/confluencebuilder/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,8 @@ def setup(app):
cm.add_conf('confluence_permit_raw_html', 'confluence')
# Remove a detected title from generated documents.
cm.add_conf_bool('confluence_remove_title', 'confluence')
# Macro configuration for Confluence-managed inlined tab content.
cm.add_conf('confluence_tab_macro', 'confluence')

# (configuration - third-party related)
# Wrap Mermaid nodes into HTML macros.
Expand Down
25 changes: 22 additions & 3 deletions sphinxcontrib/confluencebuilder/config/checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from sphinxcontrib.confluencebuilder.config.exceptions import ConfluenceHeaderFileConfigError
from sphinxcontrib.confluencebuilder.config.exceptions import ConfluenceJiraServersConfigError
from sphinxcontrib.confluencebuilder.config.exceptions import ConfluenceLatexMacroInvalidConfigError
from sphinxcontrib.confluencebuilder.config.exceptions import ConfluenceLatexMacroMissingKeysConfigError
from sphinxcontrib.confluencebuilder.config.exceptions import ConfluenceMacroMissingKeysConfigError
from sphinxcontrib.confluencebuilder.config.exceptions import ConfluencePageGenerationNoticeConfigError
from sphinxcontrib.confluencebuilder.config.exceptions import ConfluencePageSearchModeConfigError
from sphinxcontrib.confluencebuilder.config.exceptions import ConfluenceParentPageConfigError
Expand Down Expand Up @@ -392,8 +392,9 @@ def validate_configuration(builder):

if not all(name in conf_keys for name in required_keys):
keys_str = '\n - '.join(required_keys)
raise ConfluenceLatexMacroMissingKeysConfigError(keys_str) \
from ex
cfg_name = 'confluence_latex_macro'
raise ConfluenceMacroMissingKeysConfigError(
cfg_name, keys_str) from ex

# ##################################################################

Expand Down Expand Up @@ -709,6 +710,24 @@ def conf_translate(value):

# ##################################################################

# confluence_tab_macro
validator.conf('confluence_tab_macro') \
.dict_str_str()

if config.confluence_tab_macro:
conf_keys = config.confluence_tab_macro.keys()

required_keys = [
'macro-name',
]

if not all(name in conf_keys for name in required_keys):
keys_str = '\n - '.join(required_keys)
cfg_name = 'confluence_tab_macro'
raise ConfluenceMacroMissingKeysConfigError(cfg_name, keys_str)

# ##################################################################

# confluence_title_overrides
validator.conf('confluence_title_overrides') \
.dict_str_str()
Expand Down
6 changes: 3 additions & 3 deletions sphinxcontrib/confluencebuilder/config/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,10 +143,10 @@ def __init__(self):
''')


class ConfluenceLatexMacroMissingKeysConfigError(ConfluenceConfigError):
def __init__(self, keys):
class ConfluenceMacroMissingKeysConfigError(ConfluenceConfigError):
def __init__(self, name, keys):
super().__init__(f'''\
missing keys in confluence_latex_macro
missing keys in {name}

The following keys are required:

Expand Down
108 changes: 108 additions & 0 deletions sphinxcontrib/confluencebuilder/storage/translator.py
Original file line number Diff line number Diff line change
Expand Up @@ -3069,6 +3069,38 @@ def visit_DataViewerNode(self, node):

raise nodes.SkipNode

# -------------------------------------------------------
# sphinx -- extension (third party) -- sphinx-inline-tabs
# -------------------------------------------------------

def visit_TabContainer(self, node):
# check if this is an explicit hint to start a new tab container
primary_tab = node.get('new_set')

# if we are not explicitly a new tab container, check if our previous
# sibling is a tab container; if not, consider ourselves a new
# tab container
if not primary_tab:
prev_sibling = None
for child in node.parent.children:
if child is node:
if not isinstance(prev_sibling, type(node)):
primary_tab = True
break
prev_sibling = child

label_node = node.next_node()
if isinstance(label_node, nodes.label):
tabname = label_node.astext()
else:
tabname = ''

self._build_tab(node, tabname, primary_tab)

@depart_auto_context_decorator()
def depart_TabContainer(self, node):
pass

# -------------------------------------------------
# sphinx -- extension (third party) -- sphinx-needs
# -------------------------------------------------
Expand All @@ -3079,6 +3111,43 @@ def visit_PassthroughTextElement(self, node):
def depart_PassthroughTextElement(self, node):
pass

# ------------------------------------------------
# sphinx -- extension (third party) -- sphinx-tabs
# ------------------------------------------------

def visit_SphinxTabsTablist(self, node):
self._sphinxtabs_primary = True
self._sphinxtabs_tabnames = {}

for child in node.children:
tab_id = child.get('name')
if tab_id:
self._sphinxtabs_tabnames[tab_id] = child.astext()

raise nodes.SkipNode

def visit_SphinxTabsTab(self, node):
pass

def depart_SphinxTabsTab(self, node):
pass

def depart_SphinxTabsTablist(self, node):
pass

def visit_SphinxTabsPanel(self, node):
primary_tab = self._sphinxtabs_primary
self._sphinxtabs_primary = False

tab_id = node.get('name')
tab_name = self._sphinxtabs_tabnames.get(tab_id, '')

self._build_tab(node, tab_name, primary_tab)

@depart_auto_context_decorator()
def depart_SphinxTabsPanel(self, node):
pass

# ---------------------------------------------------
# sphinx -- extension (third party) -- sphinx-toolbox
# ---------------------------------------------------
Expand Down Expand Up @@ -3414,6 +3483,45 @@ def _build_anchor(self, node, anchor):
self.body.append(self.build_ac_param(node, '', compat_anchor))
self.body.append(self.end_ac_macro(node, suffix=''))

@visit_auto_context_decorator()
def _build_tab(self, node, tab_title, primary_tab):
"""
build an inlined tab entry

This is a helper call that is used by various Sphinx tab-related
extensions to help build an appropriate macro used to render tabbed
content.

Note: visit calls that use this hook need to ensure their respective
depart call is wrapped with `depart_auto_context_decorator`.

Args:
node: the node adding the anchor
tab_title: the title to use for the tab
primary_tab: whether this is the primary/first tab
"""

if not self.builder.config.confluence_tab_macro:
self._warnref(node, 'ignoring node since no tab macro configured')
raise nodes.SkipNode

conf = self.builder.config.confluence_tab_macro
macro = conf['macro-name']
pid = conf.get('primary-id')
pval = conf.get('primary-value')
tid = conf.get('title-id')

self.body.append(self.start_ac_macro(node, macro))
self.auto_append(self.end_tag(node))

if pid and primary_tab:
self.body.append(self.build_ac_param(node, pid, pval))
if tid:
self.body.append(self.build_ac_param(node, tid, tab_title))

self.body.append(self.start_ac_rich_text_body_macro(node))
self.auto_append(self.end_ac_rich_text_body_macro(node))

def start_tag(self, node, tag, suffix=None, empty=False, **kwargs):
"""
generates start tag content for a given node
Expand Down
25 changes: 20 additions & 5 deletions sphinxcontrib/confluencebuilder/translator.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,11 +165,7 @@ def unknown_visit(self, node):
handler[node_name](self, node)
raise nodes.SkipNode

if node.source:
lpf = f'#{node.line}' if node.line else ''
self.warn(f'unknown node {node_name}: {node.source}{lpf}')
else:
self.warn(f'unknown node {node_name}: {self.docname}')
self._warnref(node, f'unknown node {node_name}')

raise nodes.SkipNode

Expand Down Expand Up @@ -506,3 +502,22 @@ def _fetch_alignment(self, node):
alignment = self.encode(alignment)

return alignment

def _warnref(self, node, msg):
"""
generate a warning with a reference to the source/line number (or doc)

A helper used to generate a warning that includes the source with line
number or a document name. This is to make it easier for a user to know
where the warning originated from.

Args:
node: the node
msg: the message
"""

if node.source:
lpf = f'#{node.line}' if node.line else ''
self.warn(f'{msg}: {node.source}{lpf}')
else:
self.warn(f'{msg}: {self.docname}')
3 changes: 3 additions & 0 deletions tests/sample-sets/sphinx-inline-tabs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# sphinx-inline-tabs

The following checks the use of sphinx-inline-tabs directives.
11 changes: 11 additions & 0 deletions tests/sample-sets/sphinx-inline-tabs/conf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
extensions = [
'sphinx_inline_tabs',
'sphinxcontrib.confluencebuilder',
]

confluence_tab_macro = {
'macro-name': 'cfm-tabs-page',
'primary-id': 'primaryTab',
'primary-value': 'true',
'title-id': 'tabsPageTitle',
}
29 changes: 29 additions & 0 deletions tests/sample-sets/sphinx-inline-tabs/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
sphinx-inline-tabs
==================

.. tab:: One

One is an interesting number.

.. tab:: Two

Two is the even prime.

This will break the tab set!

.. tab:: Three

Three is an odd prime.

.. tab:: Four

Four is not a perfect number.

.. tab:: Five
:new-set:

Five is a nice number.

.. tab:: Six

Six is also nice.
Loading