Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
0f3b8e4
Enable CMD.EXE direct usage in Makefile; Remove make.bat; Update blac…
neflyte Oct 24, 2024
468cfea
Merge remote-tracking branch 'mattermost/master'
neflyte Dec 8, 2024
3e7c02d
Update master
neflyte Apr 7, 2025
60d1dc8
Merge remote-tracking branch 'mattermost/master'
neflyte May 2, 2025
98a90b7
Merge remote-tracking branch 'mattermost/master'
neflyte Jun 8, 2025
cf526dd
Merge remote-tracking branch 'mattermost/master'
neflyte Jun 14, 2025
ed58f2c
Merge remote-tracking branch 'mattermost/master'
neflyte Jun 22, 2025
35544d3
Merge remote-tracking branch 'mattermost/master'
neflyte Jul 6, 2025
e91eb10
Merge mattermost/master into this branch and resolve conflicts
neflyte Jul 7, 2025
a8bc5c1
Merge remote-tracking branch 'mattermost/master'
neflyte Aug 12, 2025
e6fceb4
Merge remote-tracking branch 'mattermost/master'
neflyte Sep 20, 2025
4654460
Merge remote-tracking branch 'mattermost/master'
neflyte Oct 12, 2025
f625482
Click on the parent tab first, if any, before clicking on the desired…
neflyte Oct 12, 2025
32317f9
Ensure tab IDs are unique per-document; Improve documentation in tab_…
neflyte Oct 12, 2025
b5e78b6
Merge branch 'master' into inline-tabs-deep-links
cwarnermm Oct 14, 2025
5785821
On page load, handle the window's hash if it's defined
neflyte Oct 15, 2025
8083b77
Merge branch 'master' into inline-tabs-deep-links
neflyte Oct 15, 2025
3cfb8cc
Expand the RegEx pattern for tab_name in tab_id.py and tabs.js
neflyte Oct 17, 2025
a45af48
Merge remote-tracking branch 'origin/inline-tabs-deep-links' into inl…
neflyte Oct 17, 2025
c9dbcb7
Merge branch 'master' into inline-tabs-deep-links
neflyte Oct 17, 2025
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
30 changes: 24 additions & 6 deletions extensions/sphinx_inline_tabs/directive.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
"""
Sphinx directive implementation.
"""

from typing import Final
from typing import Final, Optional

from docutils import nodes
from docutils.parsers.rst import directives
Expand All @@ -19,7 +18,8 @@

INLINE_TAB_DOCNAMES: Final[str] = "inline_tab_docnames"
"""The key in the Sphinx environment to store a list of documents with inline tabs"""

GENERATED_TAB_IDS: Final[str] = "generated_tab_ids"
"""The key in the Sphinx environment to store a list of generated tab IDs"""

class TabDirective(SphinxDirective):
"""Tabbed content in Sphinx documentation."""
Expand Down Expand Up @@ -73,8 +73,17 @@ def run(self) -> list[nodes.Node]:
Walk the parsed content and add new `id` attributes on section nodes that
will be used for HTML page navigation.
"""
if not hasattr(self.env, GENERATED_TAB_IDS):
setattr(self.env, GENERATED_TAB_IDS, dict())
all_generated_tab_ids: dict[str, set[str]] = getattr(self.env, GENERATED_TAB_IDS)
if self.env.docname not in all_generated_tab_ids:
all_generated_tab_ids[self.env.docname] = set()
generated_tab_ids: set[str] = all_generated_tab_ids[self.env.docname]
self.walk_parsed_nodes(
[container], tab_name=_make_id(self.arguments[0]), docname=self.env.docname
[container],
tab_name=_make_id(self.arguments[0]),
docname=self.env.docname,
generated_tab_ids=generated_tab_ids,
)

"""
Expand All @@ -96,6 +105,7 @@ def walk_parsed_nodes(
tab_name: str = "",
docname: str = "",
tab_counter: int = 0,
generated_tab_ids: Optional[set[str]] = None,
):
"""
Recursively walk a docutils node tree, adding new `id` attributes to TabContainer and section nodes.
Expand All @@ -106,10 +116,13 @@ def walk_parsed_nodes(
:param tab_name: The name of the tab, encoded as a Sphinx `id` attribute.
:param docname: The document name.
:param tab_counter: The current tab counter.
:param generated_tab_ids: A set of generated tab IDs to ensure they are unique in the document.
:return:
"""
if generated_tab_ids is None:
generated_tab_ids = set()
for parsed_node in parsed_nodes:
node_info = (
node_info: str = (
f"type(parsed_node)={type(parsed_node)}; "
f"has_ids={True if hasattr(parsed_node, 'attributes') and parsed_node.attributes["ids"] else False}; "
f"parent={parsed_node.parent!r}"
Expand All @@ -136,13 +149,15 @@ def walk_parsed_nodes(
tab_count=tab_counter + 1,
node_id=_make_id(tab_container_name),
)
tab_id.ensure_unique(generated_tab_ids)
parsed_node.tab_counter = tab_counter + 1
parsed_node.is_parsed = True
parsed_node.tab_id = tab_id
logger.debug(
f"{LOG_PREFIX} walk_parsed_nodes({docname}|{tab_name}): [{level}/{tab_counter}] {tab_container_name}; set ids to [{str(tab_id)}]"
)
parsed_node.attributes["ids"] = [str(tab_id)]
generated_tab_ids.add(str(tab_id))

elif isinstance(parsed_node, nodes.section):
section_title: str = parsed_node.next_node(nodes.title).astext()
Expand All @@ -162,6 +177,7 @@ def walk_parsed_nodes(
tab_count=tab_counter,
node_id=node_id,
)
new_id.ensure_unique(generated_tab_ids)
logger.debug(
f"{LOG_PREFIX} walk_parsed_nodes({docname}|{tab_name}): [{level}/{tab_counter}] section={node_id}, new_id={str(new_id)}"
)
Expand All @@ -174,6 +190,7 @@ def walk_parsed_nodes(
f"{LOG_PREFIX} walk_parsed_nodes({docname}|{tab_name}): [{level}/{tab_counter}] set attributes['ids'] to {new_ids}"
)
parsed_node.attributes["ids"] = new_ids
generated_tab_ids.add(str(new_id))
else:
logger.warning(
f"{LOG_PREFIX} walk_parsed_nodes({docname}|{tab_name}): [{level}/{tab_counter}] section has no IDs; this is unexpected"
Expand All @@ -194,11 +211,12 @@ def walk_parsed_nodes(
child_tab_name,
docname,
tab_counter + 1,
generated_tab_ids,
)
else:
logger.debug(
f"{LOG_PREFIX} walk_parsed_nodes({docname}|{tab_name}): [{level}/{tab_counter}] {type(parsed_node)}(); parse {len(parsed_node.children)} children"
)
self.walk_parsed_nodes(
parsed_node.children, level + 1, tab_name, docname, tab_counter
parsed_node.children, level + 1, tab_name, docname, tab_counter, generated_tab_ids
)
19 changes: 14 additions & 5 deletions extensions/sphinx_inline_tabs/static/tabs.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const labelsByName = {};
const labelsById = {};

// Tab ID pattern: "itab--{tab_name}--{level}_{tab_count}-{node_id}"
const inlinetabRE = new RegExp('itab--([a-zA-Z0-9- ]+)--([0-9]+)_([0-9]+)-(.*)');
const inlinetabRE = new RegExp('itab--([a-zA-Z0-9-.:,\'\(\) ]+)--([0-9]+)_([0-9]+)-(.*)');
const SCROLL_CURRENT = "scroll-current";

/**
Expand Down Expand Up @@ -114,6 +114,11 @@ function ready() {

// Reset the active section in the ToC
updateScrollCurrentForHash("");

// If there's a hash in the URL, handle it now
if (window.location.hash !== "") {
onHashchange();
}

// Register the hashchange handler
window.addEventListener("hashchange", onHashchange);
Expand Down Expand Up @@ -165,8 +170,6 @@ function onHashchange() {
if (hash in labelsByName && labelsByName[hash].length > 0) {
const labelElement = labelsByName[hash][0];
if (labelElement) {
console.debug(`sphinx_inline_tabs: labelsByName[${hash}][0].click()`);
labelElement.click();
// If the TabId contains the name of a parent tab, extract it and click its label
let maybeParentId = tabId.tabName.replace(`-${tabId.nodeId}`, "");
console.debug(`sphinx_inline_tabs: maybeParentId=${maybeParentId}`);
Expand All @@ -177,6 +180,10 @@ function onHashchange() {
parentLabelElement.click();
}
}
// Click the desired tab
console.debug(`sphinx_inline_tabs: labelsByName[${hash}][0].click()`);
labelElement.click();
// Update the scroll position for the current hash
console.debug(`sphinx_inline_tabs: update scroll-current for hash ${hash}`);
updateScrollCurrentForHash(hash);
return;
Expand All @@ -188,8 +195,6 @@ function onHashchange() {
if (tabId.tabName in labelsById && labelsById[tabId.tabName].length > 0) {
const labelElement = labelsById[tabId.tabName][0];
if (labelElement) {
console.debug(`sphinx_inline_tabs: labelsById[${tabId.tabName}][0].click()`);
labelElement.click();
// If the TabId contains the name of a parent tab, extract it and click its label
let maybeParentId = tabId.tabName.replace(`-${tabId.nodeId}`, "");
console.debug(`sphinx_inline_tabs: maybeParentId=${maybeParentId}`);
Expand All @@ -200,6 +205,10 @@ function onHashchange() {
parentLabelElement.click();
}
}
// Click the desired tab
console.debug(`sphinx_inline_tabs: labelsById[${tabId.tabName}][0].click()`);
labelElement.click();
// Re-run the current window hash
console.debug(`sphinx_inline_tabs: re-run current window hash`);
window.location.hash = `${window.location.hash}`;
}
Expand Down
41 changes: 40 additions & 1 deletion extensions/sphinx_inline_tabs/tab_id.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import re
from dataclasses import dataclass
from typing import Final, Optional
from sphinx.util import logging
from sphinx.util.nodes import _make_id

TAB_ID_PATTERN: Final[str] = "itab--([a-zA-Z0-9-]+)--([0-9]+)_([0-9]+)-(.*)"
TAB_ID_PATTERN: Final[str] = "itab--([a-zA-Z0-9-.:,'() ]+)--([0-9]+)_([0-9]+)-(.*)"
"""Regex pattern for validating tab identifiers."""

logger: logging.SphinxLoggerAdapter = logging.getLogger(__name__)
LOG_PREFIX: Final[str] = "[sphinx_inline_tabs]"


@dataclass(slots=True)
Expand Down Expand Up @@ -35,10 +40,22 @@ def __repr__(self):

@classmethod
def is_tab_id(cls, id_string: str) -> bool:
"""
Check if the given string is a valid tab identifier.

:param id_string: The string to check.
:return: True if the string is a valid tab identifier, False otherwise.
"""
return re.match(TAB_ID_PATTERN, id_string) is not None

@classmethod
def from_str(cls, id_string: str) -> Optional["TabId"]:
"""
Create a TabId instance from a string representation.

:param id_string: The string representation of the tab identifier.
:return: A TabId instance if the string is valid, None otherwise.
"""
match: Optional[re.Match[str]] = re.match(TAB_ID_PATTERN, id_string)
if match is not None:
return cls(
Expand All @@ -48,3 +65,25 @@ def from_str(cls, id_string: str) -> Optional["TabId"]:
node_id=match.group(4),
)
return None

def ensure_unique(self, existing_ids: Optional[set[str]]) -> None:
"""
Ensure that the tab identifier is unique within the given set of existing IDs.

:param existing_ids: The set of existing tab IDs to check against.
:raises ValueError: If existing_ids is None.
"""
if existing_ids is None:
raise ValueError("existing_ids cannot be None")
if str(self) not in existing_ids:
return
logger.debug(f"{LOG_PREFIX} TabId.ensure_unique: {str(self)} is already in the set")
is_unique: bool = False
ctr: int = 1
original_node_id: str = self.node_id
while not is_unique:
self.node_id = f"{original_node_id}_{ctr}"
if str(self) not in existing_ids:
logger.debug(f"{LOG_PREFIX} TabId.ensure_unique: {str(self)} is unique after appending {ctr}")
is_unique = True
ctr += 1