sphinx-filter-tabs 0.6.0__py3-none-any.whl → 0.8.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- filter_tabs/extension.py +60 -37
- filter_tabs/static/filter_tabs.css +28 -16
- {sphinx_filter_tabs-0.6.0.dist-info → sphinx_filter_tabs-0.8.0.dist-info}/METADATA +17 -1
- sphinx_filter_tabs-0.8.0.dist-info/RECORD +9 -0
- sphinx_filter_tabs-0.6.0.dist-info/RECORD +0 -9
- {sphinx_filter_tabs-0.6.0.dist-info → sphinx_filter_tabs-0.8.0.dist-info}/WHEEL +0 -0
- {sphinx_filter_tabs-0.6.0.dist-info → sphinx_filter_tabs-0.8.0.dist-info}/entry_points.txt +0 -0
- {sphinx_filter_tabs-0.6.0.dist-info → sphinx_filter_tabs-0.8.0.dist-info}/licenses/LICENSE +0 -0
- {sphinx_filter_tabs-0.6.0.dist-info → sphinx_filter_tabs-0.8.0.dist-info}/top_level.txt +0 -0
filter_tabs/extension.py
CHANGED
|
@@ -85,7 +85,7 @@ class FilterTabsRenderer:
|
|
|
85
85
|
self.temp_blocks: list[nodes.Node] = temp_blocks
|
|
86
86
|
|
|
87
87
|
def render_html(self) -> list[nodes.Node]:
|
|
88
|
-
"""Constructs the complete
|
|
88
|
+
"""Constructs the complete, W3C valid, and ARIA-compliant docutils node tree."""
|
|
89
89
|
# Ensure a unique ID for each filter-tabs instance on a page.
|
|
90
90
|
if not hasattr(self.env, 'filter_tabs_counter'):
|
|
91
91
|
self.env.filter_tabs_counter = 0
|
|
@@ -104,67 +104,89 @@ class FilterTabsRenderer:
|
|
|
104
104
|
}
|
|
105
105
|
style_string = "; ".join([f"{key}: {value}" for key, value in style_vars.items()])
|
|
106
106
|
|
|
107
|
-
# If debug mode is on, log the generated ID and styles
|
|
107
|
+
# If debug mode is on, log the generated ID and styles.
|
|
108
108
|
if config.filter_tabs_debug_mode:
|
|
109
109
|
logger.info(f"[sphinx-filter-tabs] ID: {group_id}, Styles: '{style_string}'")
|
|
110
110
|
|
|
111
111
|
# Create the main container node with the inline style for theming.
|
|
112
112
|
container = ContainerNode(classes=[SFT_CONTAINER], style=style_string)
|
|
113
113
|
|
|
114
|
-
# Build the semantic structure using fieldset and a hidden legend
|
|
114
|
+
# Build the semantic structure using fieldset and a hidden legend.
|
|
115
115
|
fieldset = FieldsetNode()
|
|
116
116
|
legend = LegendNode()
|
|
117
117
|
legend += nodes.Text(f"Filter by: {', '.join(self.tab_names)}")
|
|
118
118
|
fieldset += legend
|
|
119
|
-
|
|
120
|
-
#
|
|
119
|
+
|
|
120
|
+
# --- CSS Generation ---
|
|
121
|
+
# This generates the dynamic CSS that handles the core filtering logic.
|
|
121
122
|
css_rules = []
|
|
122
123
|
for tab_name in self.tab_names:
|
|
123
124
|
radio_id = f"{group_id}-{self._css_escape(tab_name)}"
|
|
124
|
-
|
|
125
|
-
#
|
|
125
|
+
panel_id = f"{radio_id}-panel"
|
|
126
|
+
# This rule finds the tab bar that contains the checked radio button,
|
|
127
|
+
# then finds its sibling content area and shows the correct panel inside.
|
|
126
128
|
css_rules.append(
|
|
127
|
-
f".{SFT_TAB_BAR}:has(#{radio_id}:checked) ~ "
|
|
128
|
-
f".{SFT_CONTENT} > .{SFT_PANEL}[data-filter='{tab_name}'] {{ display: block; }}"
|
|
129
|
+
f".{SFT_TAB_BAR}:has(#{radio_id}:checked) ~ .sft-content > #{panel_id} {{ display: block; }}"
|
|
129
130
|
)
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
131
|
+
|
|
132
|
+
# Write the dynamic CSS to a temporary file and add it to the build.
|
|
133
|
+
css_content = ''.join(css_rules)
|
|
134
|
+
static_dir = Path(self.env.app.outdir) / '_static'
|
|
135
|
+
static_dir.mkdir(parents=True, exist_ok=True)
|
|
136
|
+
css_filename = f"dynamic-filter-tabs-{group_id}.css"
|
|
137
|
+
(static_dir / css_filename).write_text(css_content, encoding='utf-8')
|
|
138
|
+
self.env.app.add_css_file(css_filename)
|
|
139
|
+
|
|
140
|
+
# --- ARIA-Compliant HTML Structure ---
|
|
141
|
+
# The tab bar container now gets the role="tablist".
|
|
134
142
|
tab_bar = nodes.container(classes=[SFT_TAB_BAR], role='tablist')
|
|
135
|
-
|
|
143
|
+
fieldset += tab_bar
|
|
144
|
+
|
|
145
|
+
# The content area holds all the panels.
|
|
146
|
+
content_area = nodes.container(classes=[SFT_CONTENT])
|
|
147
|
+
fieldset += content_area
|
|
148
|
+
|
|
149
|
+
# Map tab names to their content blocks for easy lookup.
|
|
150
|
+
content_map = {block['filter-name']: block.children for block in self.temp_blocks}
|
|
151
|
+
|
|
152
|
+
# 1. Create all radio buttons and labels first and add them to the tab_bar.
|
|
153
|
+
for i, tab_name in enumerate(self.tab_names):
|
|
136
154
|
radio_id = f"{group_id}-{self._css_escape(tab_name)}"
|
|
137
|
-
|
|
155
|
+
panel_id = f"{radio_id}-panel"
|
|
156
|
+
|
|
157
|
+
# The radio button is for state management.
|
|
138
158
|
radio = RadioInputNode(type='radio', name=group_id, ids=[radio_id])
|
|
139
|
-
|
|
140
|
-
|
|
159
|
+
|
|
160
|
+
is_default = (self.default_tab == tab_name) or (i == 0 and not self.default_tab)
|
|
161
|
+
if is_default:
|
|
162
|
+
radio['checked'] = 'checked'
|
|
141
163
|
tab_bar += radio
|
|
142
164
|
|
|
143
|
-
# The
|
|
144
|
-
label = LabelNode(for_id=radio_id, role='tab')
|
|
165
|
+
# The label is the visible tab. It gets role="tab" and aria-controls.
|
|
166
|
+
label = LabelNode(for_id=radio_id, role='tab', **{'aria-controls': panel_id})
|
|
167
|
+
if is_default:
|
|
168
|
+
label['aria-selected'] = 'true'
|
|
145
169
|
label += nodes.Text(tab_name)
|
|
146
170
|
tab_bar += label
|
|
147
|
-
fieldset += tab_bar
|
|
148
171
|
|
|
149
|
-
# Create
|
|
150
|
-
|
|
151
|
-
# Map tab names to their content blocks for easy lookup.
|
|
152
|
-
content_map = {block['filter-name']: block.children for block in self.temp_blocks}
|
|
153
|
-
# Ensure we create panels for all declared tabs plus the "General" tab.
|
|
154
|
-
all_tab_names = self.tab_names + ["General"]
|
|
172
|
+
# 2. Create all tab panels and add them to the content_area.
|
|
173
|
+
all_tab_names = ["General"] + self.tab_names
|
|
155
174
|
for tab_name in all_tab_names:
|
|
156
|
-
panel
|
|
175
|
+
# The "General" panel does not correspond to a specific tab control.
|
|
176
|
+
if tab_name == "General":
|
|
177
|
+
panel = PanelNode(classes=[SFT_PANEL], **{'data-filter': tab_name})
|
|
178
|
+
else:
|
|
179
|
+
radio_id = f"{group_id}-{self._css_escape(tab_name)}"
|
|
180
|
+
panel_id = f"{radio_id}-panel"
|
|
181
|
+
# The panel gets role="tabpanel" and is linked back to the label.
|
|
182
|
+
panel = PanelNode(classes=[SFT_PANEL], ids=[panel_id], role='tabpanel', **{'aria-labelledby': radio_id})
|
|
183
|
+
|
|
157
184
|
if tab_name in content_map:
|
|
158
|
-
# Use deepcopy to prevent docutils node mutation bugs. Since the same content
|
|
159
|
-
# might be referenced or processed multiple times, a deep copy ensures that
|
|
160
|
-
# each panel gets a completely independent set of nodes.
|
|
161
185
|
panel.extend(copy.deepcopy(content_map[tab_name]))
|
|
162
186
|
content_area += panel
|
|
163
|
-
fieldset += content_area
|
|
164
|
-
container.children = [fieldset]
|
|
165
187
|
|
|
166
|
-
|
|
167
|
-
return [
|
|
188
|
+
container.children = [fieldset]
|
|
189
|
+
return [container]
|
|
168
190
|
|
|
169
191
|
def render_fallback(self) -> list[nodes.Node]:
|
|
170
192
|
"""Renders content as a series of simple admonitions for non-HTML builders (e.g., LaTeX/PDF)."""
|
|
@@ -226,9 +248,10 @@ class FilterTabsDirective(Directive):
|
|
|
226
248
|
and delegates the final rendering to the FilterTabsRenderer.
|
|
227
249
|
"""
|
|
228
250
|
env = self.state.document.settings.env
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
251
|
+
|
|
252
|
+
# Remove comments to prevent nesting of filter-tabs directives.
|
|
253
|
+
#if hasattr(env, 'sft_context') and env.sft_context:
|
|
254
|
+
# raise self.error("Nesting `filter-tabs` is not supported.")
|
|
232
255
|
|
|
233
256
|
# Set a context flag to indicate that we are inside a filter-tabs block.
|
|
234
257
|
if not hasattr(env, 'sft_context'):
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
/* Sphinx Filter Tabs -- Static Stylesheet */
|
|
2
|
-
/* This file provides the structural and base theme styles for the component.
|
|
3
|
-
The core filtering logic (showing the active panel) is handled by a small,
|
|
4
|
-
dynamically generated <style> block created by extension.py. */
|
|
2
|
+
/* This file provides the structural and base theme styles for the component. */
|
|
5
3
|
|
|
6
4
|
/* Main container for the tab component */
|
|
7
5
|
.sft-container {
|
|
@@ -18,9 +16,19 @@
|
|
|
18
16
|
/* The <legend> is visually hidden but essential for screen reader users. */
|
|
19
17
|
.sft-legend { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
|
|
20
18
|
|
|
21
|
-
/* The <input type="radio"> buttons are the functional core
|
|
19
|
+
/* The <input type="radio"> buttons are the functional core.
|
|
20
|
+
They are hidden accessibly, not with display:none, so they remain
|
|
21
|
+
focusable for keyboard users. */
|
|
22
22
|
.sft-tab-bar > input[type="radio"] {
|
|
23
|
-
|
|
23
|
+
position: absolute;
|
|
24
|
+
width: 1px;
|
|
25
|
+
height: 1px;
|
|
26
|
+
padding: 0;
|
|
27
|
+
margin: -1px;
|
|
28
|
+
overflow: hidden;
|
|
29
|
+
clip: rect(0, 0, 0, 0);
|
|
30
|
+
white-space: nowrap;
|
|
31
|
+
border: 0;
|
|
24
32
|
}
|
|
25
33
|
|
|
26
34
|
/* The tab bar containing the clickable labels. */
|
|
@@ -40,32 +48,38 @@
|
|
|
40
48
|
color: #555;
|
|
41
49
|
font-weight: 500;
|
|
42
50
|
/* THEME: The font size is controlled by a CSS variable. */
|
|
43
|
-
font-size: var(--sft-
|
|
51
|
+
font-size: var(--sft-font-size, 1em);
|
|
44
52
|
line-height: 1.5;
|
|
45
53
|
}
|
|
46
54
|
|
|
47
|
-
/* THEME: The active tab label is highlighted using a CSS variable for the color.
|
|
48
|
-
The adjacent sibling selector (+) is robust and efficient. */
|
|
55
|
+
/* THEME: The active tab label is highlighted using a CSS variable for the color. */
|
|
49
56
|
.sft-tab-bar > input[type="radio"]:checked + label {
|
|
50
57
|
border-bottom-color: var(--sft-tab-highlight-color, #007bff);
|
|
51
58
|
color: #000;
|
|
52
59
|
}
|
|
53
60
|
|
|
61
|
+
/* Add a clear, visible focus ring for keyboard navigation. */
|
|
62
|
+
.sft-tab-bar > input[type="radio"]:focus + label {
|
|
63
|
+
box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.6);
|
|
64
|
+
border-radius: 5px;
|
|
65
|
+
outline: none;
|
|
66
|
+
}
|
|
67
|
+
|
|
54
68
|
/* The container for all content panels. */
|
|
55
69
|
.sft-content { padding: 20px; }
|
|
56
70
|
|
|
57
|
-
/* By default, hide all tab-specific panels. */
|
|
58
|
-
.sft-
|
|
71
|
+
/* By default, hide all tab-specific panels. The dynamic CSS will show the active one. */
|
|
72
|
+
.sft-content > .sft-panel {
|
|
59
73
|
display: none;
|
|
60
74
|
}
|
|
61
|
-
|
|
62
|
-
|
|
75
|
+
|
|
76
|
+
/* Always show the "General" panel, as it has no radio button to control it. */
|
|
77
|
+
.sft-content > .sft-panel[data-filter="General"] {
|
|
63
78
|
display: block;
|
|
64
79
|
}
|
|
65
|
-
/* The dynamically generated <style> block handles showing the one active panel. */
|
|
66
80
|
|
|
67
81
|
|
|
68
|
-
/* Styles for collapsible sections
|
|
82
|
+
/* Styles for collapsible sections */
|
|
69
83
|
.collapsible-section {
|
|
70
84
|
border: 1px solid #e0e0e0;
|
|
71
85
|
/* THEME: The accent color is controlled by a CSS variable. */
|
|
@@ -81,11 +95,9 @@
|
|
|
81
95
|
background-color: #f9f9f9;
|
|
82
96
|
outline: none;
|
|
83
97
|
}
|
|
84
|
-
/* Hide the default disclosure triangle in browsers like Chrome/Safari. */
|
|
85
98
|
.collapsible-section summary::-webkit-details-marker {
|
|
86
99
|
display: none;
|
|
87
100
|
}
|
|
88
|
-
/* Hide the default disclosure triangle in browsers like Firefox. */
|
|
89
101
|
.collapsible-section summary {
|
|
90
102
|
list-style: none;
|
|
91
103
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sphinx-filter-tabs
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: A Sphinx extension for accessible, JS-free filterable content tabs.
|
|
5
5
|
Author-email: Aputsiak Niels Janussen <aputsiak+sphinx@gmail.com>
|
|
6
6
|
License: GNU General Public License v3.0
|
|
@@ -45,3 +45,19 @@ This extension provides `filter-tabs` and `tab` directives to create user-friend
|
|
|
45
45
|
You can install this extension using `pip`:
|
|
46
46
|
```bash
|
|
47
47
|
pip install sphinx-filter-tabs
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Development
|
|
51
|
+
|
|
52
|
+
1. You can install a local version of the Sphinx with extension using:
|
|
53
|
+
```bash
|
|
54
|
+
./scripts/setup_dev.sh # Initially cleans previous folders in _docs/build and venv.
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Command to enter venv is provided.
|
|
58
|
+
|
|
59
|
+
2. Once inside virtual environment, you can use following commands:
|
|
60
|
+
```bash
|
|
61
|
+
pytest # Runs test suite on configured version of Sphinx.
|
|
62
|
+
tox # Check across multiple Sphinx versions. Manual install of tox required.
|
|
63
|
+
```
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
filter_tabs/__init__.py,sha256=VPpIhj4HaLeMX7ai7dZFkUm81ii2ePPGjCd9hsMjsN4,397
|
|
2
|
+
filter_tabs/extension.py,sha256=K7w7iRN72PGVvswDuULBxxxam4zvPF1ZulsAxeHlvz8,20937
|
|
3
|
+
filter_tabs/static/filter_tabs.css,sha256=gXQk2ceyjAj9p3G_k-mrcPdxq0ggIXSMuXHyUE5mPP0,3486
|
|
4
|
+
sphinx_filter_tabs-0.8.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
5
|
+
sphinx_filter_tabs-0.8.0.dist-info/METADATA,sha256=SjLifkJnouYp8U4_te9JgpPtELGgQcaaeXEXoGRSCkU,2705
|
|
6
|
+
sphinx_filter_tabs-0.8.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
7
|
+
sphinx_filter_tabs-0.8.0.dist-info/entry_points.txt,sha256=za_bQcueY8AHyq7XnnjkW9X3C-LsZjeERVQ_ds7jV1A,62
|
|
8
|
+
sphinx_filter_tabs-0.8.0.dist-info/top_level.txt,sha256=K0Iy-6EsYYdvlyXdsJT0SQg-BLDBgT5-Y8ZKy5SNAfc,12
|
|
9
|
+
sphinx_filter_tabs-0.8.0.dist-info/RECORD,,
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
filter_tabs/__init__.py,sha256=VPpIhj4HaLeMX7ai7dZFkUm81ii2ePPGjCd9hsMjsN4,397
|
|
2
|
-
filter_tabs/extension.py,sha256=ZWq-MCft-RvZtx4xzVyDGXGGtgddh8dZdeHTivIcN34,20204
|
|
3
|
-
filter_tabs/static/filter_tabs.css,sha256=btJoI-Q87e0XUQARwJUhM6RCfg5m2dvxKtfABZ2zRsc,3429
|
|
4
|
-
sphinx_filter_tabs-0.6.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
5
|
-
sphinx_filter_tabs-0.6.0.dist-info/METADATA,sha256=msO5UEdrQPPLpUF3d0aRI-HaO9gqLccVfTbw3bMGtVQ,2264
|
|
6
|
-
sphinx_filter_tabs-0.6.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
7
|
-
sphinx_filter_tabs-0.6.0.dist-info/entry_points.txt,sha256=za_bQcueY8AHyq7XnnjkW9X3C-LsZjeERVQ_ds7jV1A,62
|
|
8
|
-
sphinx_filter_tabs-0.6.0.dist-info/top_level.txt,sha256=K0Iy-6EsYYdvlyXdsJT0SQg-BLDBgT5-Y8ZKy5SNAfc,12
|
|
9
|
-
sphinx_filter_tabs-0.6.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|