sphinx-filter-tabs 1.2.5__py3-none-any.whl → 1.3__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 CHANGED
@@ -7,6 +7,7 @@ Consolidated version containing directives, data models, nodes, and Sphinx integ
7
7
  from __future__ import annotations
8
8
  import copy
9
9
  import shutil
10
+ import warnings
10
11
  from pathlib import Path
11
12
  from docutils import nodes
12
13
  from docutils.parsers.rst import Directive, directives
@@ -21,6 +22,12 @@ from . import __version__
21
22
  if TYPE_CHECKING:
22
23
  from sphinx.environment import BuildEnvironment
23
24
 
25
+ # Define the warning for Sphinx 9 compatibility
26
+ try:
27
+ from sphinx.deprecation import RemovedInSphinx11Warning
28
+ except ImportError:
29
+ RemovedInSphinx11Warning = DeprecationWarning
30
+
24
31
  # =============================================================================
25
32
  # Custom Docutils Nodes
26
33
  # =============================================================================
@@ -43,7 +50,6 @@ class SummaryNode(nodes.General, nodes.Element): pass
43
50
  class TabData:
44
51
  """
45
52
  Represents a single tab's data within a filter-tabs directive.
46
-
47
53
  Attributes:
48
54
  name: Display name of the tab
49
55
  is_default: Whether this tab should be selected by default
@@ -99,7 +105,6 @@ class FilterTabsConfig:
99
105
  class IDGenerator:
100
106
  """
101
107
  Centralized ID generation for consistent element identification.
102
-
103
108
  This ensures all IDs follow a consistent pattern and are unique
104
109
  within their filter-tabs group.
105
110
  """
@@ -151,7 +156,7 @@ class TabDirective(Directive):
151
156
 
152
157
  # Validate context
153
158
  if not hasattr(env, 'sft_context') or not env.sft_context:
154
- raise self.error("`tab` can only be used inside a `filter-tabs` directive.")
159
+ raise self.error("`tab` can only be used inside a `filter-tabs` directive.")
155
160
 
156
161
  # Parse tab argument - moved from parsers.py
157
162
  try:
@@ -242,7 +247,7 @@ class FilterTabsDirective(Directive):
242
247
  # Validate tabs - moved from parsers.py
243
248
  if not tab_data_list:
244
249
  error_message = (
245
- "No `.. tab::` directives found inside `.. filter-tabs::`. "
250
+ "No `.. tab::` directives found inside `.. filter-tabs::`.\n"
246
251
  "You must include at least one tab."
247
252
  )
248
253
  if general_content:
@@ -264,8 +269,21 @@ class FilterTabsDirective(Directive):
264
269
  from .renderer import FilterTabsRenderer
265
270
  renderer = FilterTabsRenderer(self, tab_data_list, general_content, custom_legend=custom_legend)
266
271
 
272
+ # Safe builder check that suppresses Sphinx 9 warnings
273
+ builder_name = 'html' # Default assumption
274
+
275
+ try:
276
+ # We catch the specific warning about env.app being deprecated
277
+ with warnings.catch_warnings():
278
+ warnings.filterwarnings("ignore", category=RemovedInSphinx11Warning)
279
+ if hasattr(env, 'app') and env.app:
280
+ builder_name = env.app.builder.name
281
+ except Exception:
282
+ # Fallback for very old Sphinx or edge cases
283
+ pass
284
+
267
285
  # Render based on builder
268
- if env.app.builder.name == 'html':
286
+ if builder_name == 'html':
269
287
  return renderer.render_html()
270
288
  else:
271
289
  return renderer.render_fallback()
@@ -274,7 +292,7 @@ class FilterTabsDirective(Directive):
274
292
  """Validate tab data list - moved from parsers.py"""
275
293
  if not tab_data_list and not skip_empty_check:
276
294
  raise ValueError(
277
- "No tab directives found inside filter-tabs. "
295
+ "No tab directives found inside filter-tabs.\n"
278
296
  "Add at least one .. tab:: directive."
279
297
  )
280
298
  names = []
@@ -382,7 +400,7 @@ def visit_radio_input_node(self: HTML5Translator, node: RadioInputNode) -> None:
382
400
  self.body.append(self.starttag(node, 'input', **attrs))
383
401
 
384
402
  def depart_radio_input_node(self: HTML5Translator, node: RadioInputNode) -> None:
385
- pass # Self-closing tag
403
+ pass # Self-closing tag
386
404
 
387
405
  def visit_label_node(self: HTML5Translator, node: LabelNode) -> None:
388
406
  attrs = _get_html_attrs(node)
@@ -419,12 +437,31 @@ def depart_summary_node(self: HTML5Translator, node: SummaryNode) -> None:
419
437
  self.body.append('</summary>')
420
438
 
421
439
 
440
+ # =============================================================================
441
+ # Use ARIA to Improve Strong Element Behavior
442
+ # =============================================================================
443
+
444
+ def improve_inline_formatting(app: Sphinx, doctree: nodes.document, docname: str):
445
+ """Improve screen reader handling of inline formatting."""
446
+ if app.builder.name != 'html':
447
+ return
448
+
449
+ # Find all strong/emphasis nodes and add ARIA attributes
450
+ for node in doctree.findall(nodes.strong):
451
+ # Add aria-label to make the content read as one unit
452
+ text_content = node.astext()
453
+ node['aria-label'] = text_content
454
+
455
+ for node in doctree.findall(nodes.emphasis):
456
+ text_content = node.astext()
457
+ node['aria-label'] = text_content
458
+
422
459
  # =============================================================================
423
460
  # Static File Handling
424
461
  # =============================================================================
425
462
 
426
463
  def copy_static_files(app: Sphinx):
427
- """Copy CSS and JS files to the build directory."""
464
+ """Copy CSS file to the build directory."""
428
465
  if app.builder.name != 'html':
429
466
  return
430
467
 
@@ -436,11 +473,7 @@ def copy_static_files(app: Sphinx):
436
473
  css_file = static_source_dir / "filter_tabs.css"
437
474
  if css_file.exists():
438
475
  shutil.copy(css_file, dest_dir)
439
-
440
- # Copy JS file if it exists
441
- js_file = static_source_dir / "filter_tabs.js"
442
- if js_file.exists():
443
- shutil.copy(js_file, dest_dir)
476
+
444
477
 
445
478
 
446
479
  # =============================================================================
@@ -456,7 +489,6 @@ def setup(app: Sphinx) -> Dict[str, Any]:
456
489
 
457
490
  # Add static files
458
491
  app.add_css_file('filter_tabs.css')
459
- app.add_js_file('filter_tabs.js')
460
492
 
461
493
  # Register custom nodes (keep existing node registration code)
462
494
  app.add_node(ContainerNode, html=(visit_container_node, depart_container_node))
@@ -475,9 +507,11 @@ def setup(app: Sphinx) -> Dict[str, Any]:
475
507
  # Connect event handlers
476
508
  app.connect('builder-inited', copy_static_files)
477
509
  app.connect('doctree-resolved', setup_collapsible_admonitions)
510
+ app.connect('doctree-resolved', improve_inline_formatting)
478
511
 
479
512
  return {
480
513
  'version': __version__,
481
514
  'parallel_read_safe': True,
482
515
  'parallel_write_safe': True,
483
516
  }
517
+
filter_tabs/renderer.py CHANGED
@@ -88,13 +88,12 @@ class FilterTabsRenderer:
88
88
  def __init__(self, directive: Directive, tab_data: List[TabData], general_content: List[nodes.Node], custom_legend: Optional[str] = None):
89
89
  self.directive = directive
90
90
  self.env: BuildEnvironment = directive.state.document.settings.env
91
- self.app = self.env.app
92
91
  self.tab_data = tab_data
93
92
  self.general_content = general_content
94
93
  self.custom_legend = custom_legend
95
94
 
96
- # 1. Load configuration first
97
- self.config = FilterTabsConfig.from_sphinx_config(self.app.config)
95
+ # 1. Load configuration using env.config (Sphinx 9+ safe)
96
+ self.config = FilterTabsConfig.from_sphinx_config(self.env.config)
98
97
 
99
98
  # 2. Safely initialize the counter on the environment if it doesn't exist
100
99
  if not hasattr(self.env, 'filter_tabs_counter'):
@@ -149,8 +148,6 @@ class FilterTabsRenderer:
149
148
  'style': self.config.to_css_properties()
150
149
  }
151
150
 
152
- # REMOVED: _generate_compatible_css method - no longer needed!
153
-
154
151
  def _create_fieldset(self) -> FieldsetNode:
155
152
  """Create the main fieldset containing the legend, radio buttons, and panels."""
156
153
  fieldset = FieldsetNode(role="radiogroup")
@@ -196,13 +193,16 @@ class FilterTabsRenderer:
196
193
  radio_group += self._create_screen_reader_description(i, tab)
197
194
 
198
195
  def _create_radio_button(self, index: int, tab: TabData, is_checked: bool) -> RadioInputNode:
199
- """Create a single radio button input."""
196
+ """Create a single radio button input with data attribute."""
200
197
  radio = RadioInputNode(
201
198
  classes=['sr-only'],
202
199
  type='radio',
203
200
  name=self.group_id,
204
201
  ids=[self.id_gen.radio_id(index)],
205
- **{'aria-describedby': self.id_gen.desc_id(index)}
202
+ **{
203
+ 'aria-describedby': self.id_gen.desc_id(index),
204
+ 'data-tab-index': str(index) # FIXED: Add data attribute
205
+ }
206
206
  )
207
207
  if tab.aria_label:
208
208
  radio['aria-label'] = tab.aria_label
@@ -224,9 +224,16 @@ class FilterTabsRenderer:
224
224
  return description_node
225
225
 
226
226
  def _populate_content_area(self, content_area: ContainerNode) -> None:
227
- """Create and add all general and tab-specific content panels."""
227
+ """Create and add all general and tab-specific content panels with accessibility enhancements."""
228
228
  if self.general_content:
229
- general_panel = PanelNode(classes=[SFT_PANEL], **{'data-filter': 'General'})
229
+ general_panel = PanelNode(
230
+ classes=[SFT_PANEL],
231
+ **{
232
+ 'data-filter': 'General',
233
+ 'aria-label': 'General information',
234
+ 'role': 'region'
235
+ }
236
+ )
230
237
  general_panel.extend(copy.deepcopy(self.general_content))
231
238
  content_area += general_panel
232
239
 
@@ -234,15 +241,17 @@ class FilterTabsRenderer:
234
241
  content_area += self._create_tab_panel(i, tab)
235
242
 
236
243
  def _create_tab_panel(self, index: int, tab: TabData) -> PanelNode:
237
- """Create a single content panel for a tab."""
244
+ """Create a single content panel for a tab - CSS only version."""
238
245
  panel_attrs = {
239
246
  'classes': [SFT_PANEL],
240
247
  'ids': [self.id_gen.panel_id(index)],
241
248
  'role': 'tabpanel',
242
249
  'aria-labelledby': self.id_gen.radio_id(index),
243
- 'tabindex': '0',
244
- 'data-tab': tab.name.lower().replace(' ', '-')
250
+ 'tabindex': '0', # Keep for keyboard accessibility
251
+ 'data-tab': tab.name.lower().replace(' ', '-'),
252
+ 'data-tab-index': str(index)
245
253
  }
246
254
  panel = PanelNode(**panel_attrs)
247
255
  panel.extend(copy.deepcopy(tab.content))
248
256
  return panel
257
+
@@ -1,4 +1,4 @@
1
- /* Sphinx Filter Tabs - Simplified Accessibility-First Stylesheet */
1
+ /* Sphinx Filter Tabs - Enhanced CSS with improved accessibility */
2
2
 
3
3
  /* Main container */
4
4
  .sft-container {
@@ -95,11 +95,39 @@
95
95
  outline: none;
96
96
  }
97
97
 
98
- /* Focus styling for panels */
98
+ /* Enhanced focus styling for CSS-only version */
99
99
  .sft-panel:focus {
100
- outline: 2px solid var(--sft-highlight-color, #007bff);
101
- outline-offset: -2px;
100
+ outline: 3px solid var(--sft-highlight-color, #007bff);
101
+ outline-offset: 2px;
102
102
  border-radius: 4px;
103
+ background: rgba(0, 123, 255, 0.05);
104
+ /* Ensure smooth transition */
105
+ transition: background-color 0.2s ease;
106
+ }
107
+
108
+ /* Visual focus indicator for better UX */
109
+ .sft-panel:focus::before {
110
+ content: "→ ";
111
+ color: var(--sft-highlight-color, #007bff);
112
+ font-weight: bold;
113
+ margin-right: 0.5em;
114
+ }
115
+
116
+ /* Improve screen reader flow for inline formatting */
117
+ strong, b, em, i {
118
+ speak: normal;
119
+ }
120
+
121
+ /* Ensure tab panels are easily discoverable when focused */
122
+ .sft-panel[role="tabpanel"]:focus {
123
+ position: relative;
124
+ z-index: 1;
125
+ }
126
+
127
+ /* ENHANCED: Focus styling for content within panels */
128
+ .sft-panel:focus-within {
129
+ outline: 1px solid var(--sft-highlight-color, #007bff);
130
+ outline-offset: -1px;
103
131
  }
104
132
 
105
133
  /* General content panel - always visible */
@@ -110,31 +138,71 @@
110
138
  border-bottom: 1px solid #eee;
111
139
  }
112
140
 
113
- /*
114
- * FIXED: Panel visibility using CSS without inline styles
115
- * This uses a general approach with nth-child selectors
116
- * No more inline <style> elements needed!
117
- */
118
-
119
- /* Show panels when corresponding radio is checked */
120
- .sft-radio-group input[type="radio"]:nth-child(1):checked ~ .sft-content .sft-panel:nth-of-type(1),
121
- .sft-radio-group input[type="radio"]:nth-child(3):checked ~ .sft-content .sft-panel:nth-of-type(2),
122
- .sft-radio-group input[type="radio"]:nth-child(5):checked ~ .sft-content .sft-panel:nth-of-type(3),
123
- .sft-radio-group input[type="radio"]:nth-child(7):checked ~ .sft-content .sft-panel:nth-of-type(4),
124
- .sft-radio-group input[type="radio"]:nth-child(9):checked ~ .sft-content .sft-panel:nth-of-type(5),
125
- .sft-radio-group input[type="radio"]:nth-child(11):checked ~ .sft-content .sft-panel:nth-of-type(6),
126
- .sft-radio-group input[type="radio"]:nth-child(13):checked ~ .sft-content .sft-panel:nth-of-type(7),
127
- .sft-radio-group input[type="radio"]:nth-child(15):checked ~ .sft-content .sft-panel:nth-of-type(8),
128
- .sft-radio-group input[type="radio"]:nth-child(17):checked ~ .sft-content .sft-panel:nth-of-type(9),
129
- .sft-radio-group input[type="radio"]:nth-child(19):checked ~ .sft-content .sft-panel:nth-of-type(10) {
141
+ /* Panel visibility using data attributes */
142
+ .sft-radio-group input[type="radio"][data-tab-index="0"]:checked ~ .sft-content .sft-panel[data-tab-index="0"],
143
+ .sft-radio-group input[type="radio"][data-tab-index="1"]:checked ~ .sft-content .sft-panel[data-tab-index="1"],
144
+ .sft-radio-group input[type="radio"][data-tab-index="2"]:checked ~ .sft-content .sft-panel[data-tab-index="2"],
145
+ .sft-radio-group input[type="radio"][data-tab-index="3"]:checked ~ .sft-content .sft-panel[data-tab-index="3"],
146
+ .sft-radio-group input[type="radio"][data-tab-index="4"]:checked ~ .sft-content .sft-panel[data-tab-index="4"],
147
+ .sft-radio-group input[type="radio"][data-tab-index="5"]:checked ~ .sft-content .sft-panel[data-tab-index="5"],
148
+ .sft-radio-group input[type="radio"][data-tab-index="6"]:checked ~ .sft-content .sft-panel[data-tab-index="6"],
149
+ .sft-radio-group input[type="radio"][data-tab-index="7"]:checked ~ .sft-content .sft-panel[data-tab-index="7"],
150
+ .sft-radio-group input[type="radio"][data-tab-index="8"]:checked ~ .sft-content .sft-panel[data-tab-index="8"],
151
+ .sft-radio-group input[type="radio"][data-tab-index="9"]:checked ~ .sft-content .sft-panel[data-tab-index="9"] {
130
152
  display: block;
131
153
  }
132
154
 
133
- /* Alternative approach using CSS custom properties (more elegant) */
134
- /*
135
- * If you prefer a more modern approach, you could use CSS custom properties
136
- * and update the renderer to set --active-tab instead of generating CSS rules
137
- */
155
+ /* ENHANCED: Accessibility improvements for content within panels */
156
+
157
+ /* Make paragraphs within panels more accessible */
158
+ .sft-panel p {
159
+ margin: 0.75em 0;
160
+ line-height: 1.5;
161
+ }
162
+
163
+ .sft-panel p:first-child {
164
+ margin-top: 0;
165
+ }
166
+
167
+ .sft-panel p:last-child {
168
+ margin-bottom: 0;
169
+ }
170
+
171
+ /* Enhanced code block accessibility */
172
+ .sft-panel .highlight pre,
173
+ .sft-panel code {
174
+ /* Ensure code blocks are focusable and readable */
175
+ border-radius: 4px;
176
+ position: relative;
177
+ }
178
+
179
+ .sft-panel .highlight pre:focus,
180
+ .sft-panel code:focus {
181
+ outline: 2px solid var(--sft-highlight-color, #007bff);
182
+ outline-offset: 2px;
183
+ /* Ensure the content is announced to screen readers */
184
+ z-index: 1;
185
+ }
186
+
187
+ /* Make lists more accessible */
188
+ .sft-panel ul,
189
+ .sft-panel ol {
190
+ margin: 0.75em 0;
191
+ padding-left: 2em;
192
+ }
193
+
194
+ .sft-panel li {
195
+ margin: 0.25em 0;
196
+ line-height: 1.4;
197
+ }
198
+
199
+ /* Enhanced blockquote accessibility */
200
+ .sft-panel blockquote {
201
+ margin: 1em 0;
202
+ padding: 0.5em 1em;
203
+ border-left: 4px solid #ddd;
204
+ background: #f9f9f9;
205
+ }
138
206
 
139
207
  /* Screen reader only content */
140
208
  .sr-only {
@@ -149,6 +217,23 @@
149
217
  border: 0;
150
218
  }
151
219
 
220
+ /* ENHANCED: Skip link for better keyboard navigation */
221
+ .sft-container .skip-to-content {
222
+ position: absolute;
223
+ top: -40px;
224
+ left: 6px;
225
+ background: var(--sft-highlight-color, #007bff);
226
+ color: white;
227
+ padding: 8px;
228
+ text-decoration: none;
229
+ border-radius: 4px;
230
+ z-index: 1000;
231
+ }
232
+
233
+ .sft-container .skip-to-content:focus {
234
+ top: 6px;
235
+ }
236
+
152
237
  /* Collapsible sections */
153
238
  .collapsible-section {
154
239
  border: 1px solid #e0e0e0;
@@ -190,14 +275,22 @@
190
275
  padding: 15px;
191
276
  }
192
277
 
193
- /* High contrast mode support */
278
+ /* ENHANCED: High contrast mode support with better visibility */
194
279
  @media (prefers-contrast: high) {
195
280
  .sft-radio-group input[type="radio"]:checked + label {
196
281
  border-bottom-width: 4px;
282
+ background: #000;
283
+ color: #fff;
197
284
  }
198
285
 
199
286
  .sft-radio-group input[type="radio"]:focus + label {
200
287
  box-shadow: 0 0 0 4px rgba(0, 0, 0, 0.8);
288
+ outline: 3px solid #fff;
289
+ }
290
+
291
+ .sft-panel:focus {
292
+ outline: 3px solid #000;
293
+ background: #fff;
201
294
  }
202
295
  }
203
296
 
@@ -208,3 +301,24 @@
208
301
  transition: none;
209
302
  }
210
303
  }
304
+
305
+ /* ENHANCED: Screen reader specific improvements */
306
+ @media (prefers-reduced-motion: reduce) {
307
+ /* Ensure focus changes are immediate for screen reader users */
308
+ .sft-panel {
309
+ transition: none;
310
+ }
311
+ }
312
+
313
+ /* Force visibility for screen reader testing */
314
+ @media (forced-colors: active) {
315
+ .sft-panel {
316
+ border: 1px solid ButtonText;
317
+ }
318
+
319
+ .sft-radio-group input[type="radio"]:checked + label {
320
+ border: 2px solid Highlight;
321
+ background: Highlight;
322
+ color: HighlightText;
323
+ }
324
+ }
@@ -0,0 +1,267 @@
1
+ Metadata-Version: 2.4
2
+ Name: sphinx-filter-tabs
3
+ Version: 1.3
4
+ Summary: A Sphinx extension for accessible, CSS-first filterable content tabs.
5
+ Author-email: Aputsiak Niels Janussen <aputtu+sphinx@gmail.com>
6
+ License: GNU General Public License v3.0
7
+ Project-URL: Homepage, https://github.com/aputtu/sphinx-filter-tabs
8
+ Project-URL: Repository, https://github.com/aputtu/sphinx-filter-tabs.git
9
+ Project-URL: Issues, https://github.com/aputtu/sphinx-filter-tabs/issues
10
+ Keywords: sphinx,extension,tabs,filter,documentation,css-only,accessibility,keyboard-navigation
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Framework :: Sphinx :: Extension
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Documentation :: Sphinx
20
+ Classifier: Topic :: Software Development :: Documentation
21
+ Requires-Python: >=3.10
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: Sphinx<10.0,>=7.0
25
+ Dynamic: license-file
26
+
27
+ # Sphinx Filter Tabs Extension
28
+
29
+ [![Tests and Docs Deployment](https://github.com/aputtu/sphinx-filter-tabs/actions/workflows/test.yml/badge.svg)](https://github.com/aputtu/sphinx-filter-tabs/actions/workflows/test.yml)
30
+ [![PyPI version](https://img.shields.io/pypi/v/sphinx-filter-tabs.svg)](https://pypi.org/project/sphinx-filter-tabs/)
31
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/sphinx-filter-tabs.svg)](https://pypi.org/project/sphinx-filter-tabs/)
32
+ [![PyPI - License](https://img.shields.io/pypi/l/sphinx-filter-tabs.svg)](https://github.com/aputtu/sphinx-filter-tabs/blob/main/LICENSE)
33
+
34
+ A robust Sphinx extension for creating accessible, filterable content tabs using pure CSS and semantic HTML.
35
+
36
+ **📖 View extension and documentation at: https://aputtu.github.io/sphinx-filter-tabs/**
37
+
38
+ This extension provides `filter-tabs` and `tab` directives to create user-friendly, switchable content blocks. Perfect for showing code examples in multiple languages, installation instructions for different platforms, or any content that benefits from organized, filterable presentation.
39
+
40
+ ## Key Features
41
+
42
+ - **Pure CSS Implementation:** Zero JavaScript dependencies for maximum compatibility and performance
43
+ - **Fully Accessible:** WAI-ARIA compliant with native keyboard navigation and screen reader support
44
+ - **Semantic HTML:** Uses standard form controls (radio buttons) for robust, predictable behavior
45
+ - **Universal Compatibility:** Works in all environments where CSS is supported, including strict CSP policies
46
+ - **Easy Customization:** Theme colors and styling through simple CSS custom properties
47
+ - **Multiple Output Formats:** Graceful fallback to sequential content in PDF/LaTeX builds
48
+ - **Proven Reliability:** Comprehensive test suite across multiple Python and Sphinx versions
49
+
50
+ ## Quick Start
51
+
52
+ ### Installation
53
+
54
+ ```bash
55
+ pip install sphinx-filter-tabs
56
+ ```
57
+
58
+ ### Enable the Extension
59
+
60
+ Add to your `conf.py`:
61
+
62
+ ```python
63
+ extensions = [
64
+ # ... your other extensions ...
65
+ 'filter_tabs.extension',
66
+ ]
67
+ ```
68
+
69
+ ### Basic Usage
70
+
71
+ ```rst
72
+ .. filter-tabs::
73
+
74
+ This content appears above all tabs.
75
+
76
+ .. tab:: Python
77
+
78
+ Install using pip:
79
+
80
+ .. code-block:: bash
81
+
82
+ pip install my-package
83
+
84
+ .. tab:: Conda (default)
85
+
86
+ Install using conda:
87
+
88
+ .. code-block:: bash
89
+
90
+ conda install my-package
91
+
92
+ .. tab:: From Source
93
+
94
+ Build from source:
95
+
96
+ .. code-block:: bash
97
+
98
+ git clone https://github.com/user/repo.git
99
+ cd repo
100
+ pip install -e .
101
+ ```
102
+
103
+ ## Configuration Options
104
+
105
+ Add these optional settings to your `conf.py`:
106
+
107
+ ```python
108
+ # Customize the active tab highlight color
109
+ filter_tabs_highlight_color = '#007bff' # Default: '#007bff'
110
+
111
+ # Enable debug logging during development
112
+ filter_tabs_debug_mode = False # Default: False
113
+ ```
114
+
115
+ ## Advanced Usage
116
+
117
+ ### Custom Legend
118
+
119
+ Override the auto-generated legend:
120
+
121
+ ```rst
122
+ .. filter-tabs::
123
+ :legend: Select Your Installation Method
124
+
125
+ .. tab:: Quick Install
126
+ Content here...
127
+ ```
128
+
129
+ ### ARIA Labels for Accessibility
130
+
131
+ Provide descriptive labels for screen readers:
132
+
133
+ ```rst
134
+ .. filter-tabs::
135
+
136
+ .. tab:: CLI
137
+ :aria-label: Command line installation instructions
138
+
139
+ Content for command line users...
140
+ ```
141
+
142
+ ### Nested Tabs
143
+
144
+ Create complex layouts with nested tab groups:
145
+
146
+ ```rst
147
+ .. filter-tabs::
148
+
149
+ .. tab:: Windows
150
+
151
+ Choose your package manager:
152
+
153
+ .. filter-tabs::
154
+
155
+ .. tab:: Chocolatey
156
+ choco install my-package
157
+
158
+ .. tab:: Scoop
159
+ scoop install my-package
160
+ ```
161
+
162
+ ## How It Works
163
+
164
+ This extension uses a **pure CSS architecture** with semantic HTML:
165
+
166
+ - **Radio buttons** provide the selection mechanism (hidden but accessible)
167
+ - **CSS `:checked` selectors** control panel visibility
168
+ - **Fieldset/legend** structure provides semantic grouping
169
+ - **ARIA attributes** enhance screen reader support
170
+ - **Native keyboard navigation** works through standard form controls
171
+
172
+ This approach ensures:
173
+ - **Maximum compatibility** across all browsers and assistive technologies
174
+ - **Better performance** with no JavaScript parsing or execution
175
+ - **Enhanced security** for environments with strict Content Security Policies
176
+ - **Simplified maintenance** with fewer dependencies and potential conflicts
177
+
178
+ ## Browser Support
179
+
180
+ Works in all modern browsers that support:
181
+ - CSS3 selectors (`:checked`, attribute selectors)
182
+ - Basic ARIA attributes
183
+ - HTML5 form elements
184
+
185
+ This includes all browsers from the last 10+ years.
186
+
187
+ ## Development
188
+
189
+ ### Quick Setup
190
+
191
+ ```bash
192
+ git clone https://github.com/aputtu/sphinx-filter-tabs.git
193
+ cd sphinx-filter-tabs
194
+ ./scripts/setup_dev.sh
195
+ ```
196
+
197
+ This creates a virtual environment and builds the documentation.
198
+
199
+ ### Development Commands
200
+
201
+ ```bash
202
+ # Activate virtual environment
203
+ source venv/bin/activate
204
+
205
+ # Run tests
206
+ pytest
207
+
208
+ # Build documentation
209
+ ./scripts/dev.sh html
210
+
211
+ # Run tests across multiple Sphinx versions
212
+ tox
213
+
214
+ # Clean build and start fresh
215
+ ./scripts/dev.sh clean-all
216
+
217
+ # Export project structure for analysis
218
+ ./scripts/export-project.sh
219
+ ```
220
+
221
+ ### Testing
222
+
223
+ The project includes comprehensive tests covering:
224
+ - Basic tab functionality and content visibility
225
+ - Accessibility features and ARIA compliance
226
+ - Nested tabs and complex layouts
227
+ - Multiple output formats (HTML, LaTeX)
228
+ - Cross-browser compatibility
229
+
230
+ Tests run automatically on:
231
+ - Python versions 3.10, 3.12
232
+ - Sphinx versions 7.0, 7.4, 8.0, 8.2, 9.0, 9.1
233
+ - Multiple operating systems via GitHub Actions
234
+
235
+ ## Architecture
236
+
237
+ The extension consists of three main components:
238
+
239
+ - **`extension.py`** - Sphinx integration, directives, and node definitions
240
+ - **`renderer.py`** - HTML generation and output formatting
241
+ - **`static/filter_tabs.css`** - Pure CSS styling and functionality
242
+
243
+ This clean separation makes the code easy to understand, test, and maintain.
244
+
245
+ ## Contributing
246
+
247
+ Contributions are welcome! Please:
248
+
249
+ 1. Fork the repository
250
+ 2. Create a feature branch
251
+ 3. Add tests for new functionality
252
+ 4. Ensure all tests pass: `pytest`
253
+ 5. Submit a pull request
254
+
255
+ ## License
256
+
257
+ GNU General Public License v3.0. See [LICENSE](LICENSE) for details.
258
+
259
+ ## Changelog
260
+
261
+ See [CHANGELOG.md](docs/changelog.rst) for version history and migration notes.
262
+
263
+ ## Support
264
+
265
+ - **Documentation**: https://aputtu.github.io/sphinx-filter-tabs/
266
+ - **Issues**: https://github.com/aputtu/sphinx-filter-tabs/issues
267
+ - **PyPI**: https://pypi.org/project/sphinx-filter-tabs/
@@ -0,0 +1,10 @@
1
+ filter_tabs/__init__.py,sha256=VPpIhj4HaLeMX7ai7dZFkUm81ii2ePPGjCd9hsMjsN4,397
2
+ filter_tabs/extension.py,sha256=dcSbnQgCy0TL2eOoFvP7eN3gVCRwtLrs4QLHXKK9bGM,19428
3
+ filter_tabs/renderer.py,sha256=sapT3k-pAkVIXRypuTrtuc8XHZB23Sp8mgOSU3ZIqGg,10096
4
+ filter_tabs/static/filter_tabs.css,sha256=ZoiSWcn2YBEWgkQ-vPbIPHwQ7s2TG4aUikyxM1A8b9I,7956
5
+ sphinx_filter_tabs-1.3.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
6
+ sphinx_filter_tabs-1.3.dist-info/METADATA,sha256=BEz8EQjjQea2bXmfX2OERvbgxsjddTdE_vw69zjUqbs,7635
7
+ sphinx_filter_tabs-1.3.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
8
+ sphinx_filter_tabs-1.3.dist-info/entry_points.txt,sha256=za_bQcueY8AHyq7XnnjkW9X3C-LsZjeERVQ_ds7jV1A,62
9
+ sphinx_filter_tabs-1.3.dist-info/top_level.txt,sha256=K0Iy-6EsYYdvlyXdsJT0SQg-BLDBgT5-Y8ZKy5SNAfc,12
10
+ sphinx_filter_tabs-1.3.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,101 +0,0 @@
1
- // Progressive enhancement for focus management and accessibility announcements.
2
- // This file provides enhancements while maintaining native radio button keyboard behavior.
3
-
4
- (function() {
5
- 'use strict';
6
-
7
- // Only enhance if the extension's HTML is present on the page.
8
- if (!document.querySelector('.sft-container')) return;
9
-
10
- /**
11
- * Moves focus to the content panel associated with a given radio button.
12
- * This improves accessibility by directing screen reader users to the new content.
13
- * @param {HTMLInputElement} radio The radio button that was selected.
14
- */
15
- function focusOnPanel(radio) {
16
- if (!radio.checked) return;
17
-
18
- // Derive the panel's ID from the radio button's ID.
19
- // e.g., 'filter-group-1-radio-0' becomes 'filter-group-1-panel-0'
20
- const panelId = radio.id.replace('-radio-', '-panel-');
21
- const panel = document.getElementById(panelId);
22
-
23
- if (panel) {
24
- panel.focus();
25
- }
26
- }
27
-
28
- /**
29
- * Creates or updates a live region to announce tab changes to screen readers.
30
- * @param {string} tabName The name of the selected tab.
31
- */
32
- function announceTabChange(tabName) {
33
- // Create or find the live region for screen reader announcements.
34
- let liveRegion = document.getElementById('tab-live-region');
35
- if (!liveRegion) {
36
- liveRegion = document.createElement('div');
37
- liveRegion.id = 'tab-live-region';
38
- liveRegion.setAttribute('role', 'status');
39
- liveRegion.setAttribute('aria-live', 'polite');
40
- liveRegion.setAttribute('aria-atomic', 'true');
41
- // Hide the element visually but keep it accessible.
42
- liveRegion.style.position = 'absolute';
43
- liveRegion.style.left = '-10000px';
44
- liveRegion.style.width = '1px';
45
- liveRegion.style.height = '1px';
46
- liveRegion.style.overflow = 'hidden';
47
- document.body.appendChild(liveRegion);
48
- }
49
-
50
- // Update the announcement text.
51
- liveRegion.textContent = `${tabName} tab selected`;
52
-
53
- // Clear the announcement after a short delay to prevent clutter.
54
- setTimeout(() => {
55
- liveRegion.textContent = '';
56
- }, 1000);
57
- }
58
-
59
- /**
60
- * Initializes progressive enhancements for all filter-tab components.
61
- * REMOVED: Custom keyboard navigation (now uses native radio button behavior)
62
- * KEPT: Focus management and screen reader announcements
63
- */
64
- function initTabEnhancements() {
65
- const containers = document.querySelectorAll('.sft-container');
66
-
67
- containers.forEach(container => {
68
- const tabBar = container.querySelector('.sft-radio-group');
69
- if (!tabBar) return;
70
-
71
- const radios = tabBar.querySelectorAll('input[type="radio"]');
72
- const labels = tabBar.querySelectorAll('label');
73
-
74
- if (radios.length === 0 || labels.length === 0) return;
75
-
76
- // Add change listeners for announcements and focus management
77
- radios.forEach((radio, index) => {
78
- radio.addEventListener('change', () => {
79
- if (radio.checked) {
80
- // Get the tab name from the associated label
81
- const label = labels[index];
82
- const tabName = label ? label.textContent.trim() : 'Unknown';
83
-
84
- // Announce the change to screen readers
85
- announceTabChange(tabName);
86
-
87
- // Move focus to the newly visible panel
88
- focusOnPanel(radio);
89
- }
90
- });
91
- });
92
- });
93
- }
94
-
95
- // Initialize the enhancements once the DOM is ready
96
- if (document.readyState === 'loading') {
97
- document.addEventListener('DOMContentLoaded', initTabEnhancements);
98
- } else {
99
- initTabEnhancements();
100
- }
101
- })();
@@ -1,70 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: sphinx-filter-tabs
3
- Version: 1.2.5
4
- Summary: A Sphinx extension for accessible, CSS-first filterable content tabs.
5
- Author-email: Aputsiak Niels Janussen <aputtu+sphinx@gmail.com>
6
- License: GNU General Public License v3.0
7
- Project-URL: Homepage, https://github.com/aputtu/sphinx-filter-tabs
8
- Project-URL: Repository, https://github.com/aputtu/sphinx-filter-tabs.git
9
- Project-URL: Issues, https://github.com/aputtu/sphinx-filter-tabs/issues
10
- Keywords: sphinx,extension,tabs,filter,documentation,css-only,accessibility,keyboard-navigation
11
- Classifier: Development Status :: 5 - Production/Stable
12
- Classifier: Framework :: Sphinx :: Extension
13
- Classifier: Intended Audience :: Developers
14
- Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
15
- Classifier: Operating System :: OS Independent
16
- Classifier: Programming Language :: Python :: 3
17
- Classifier: Programming Language :: Python :: 3.10
18
- Classifier: Programming Language :: Python :: 3.12
19
- Classifier: Topic :: Documentation :: Sphinx
20
- Classifier: Topic :: Software Development :: Documentation
21
- Requires-Python: >=3.10
22
- Description-Content-Type: text/markdown
23
- License-File: LICENSE
24
- Requires-Dist: Sphinx<9.0,>=7.0
25
- Dynamic: license-file
26
-
27
- # Sphinx Filter Tabs Extension
28
-
29
- [![Tests and Docs Deployment](https://github.com/aputtu/sphinx-filter-tabs/actions/workflows/test.yml/badge.svg)](https://github.com/aputtu/sphinx-filter-tabs/actions/workflows/test.yml)
30
- [![PyPI version](https://img.shields.io/pypi/v/sphinx-filter-tabs.svg)](https://pypi.org/project/sphinx-filter-tabs/)
31
- [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/sphinx-filter-tabs.svg)](https://pypi.org/project/sphinx-filter-tabs/)
32
- [![PyPI - License](https://img.shields.io/pypi/l/sphinx-filter-tabs.svg)](https://github.com/aputtu/sphinx-filter-tabs/blob/main/LICENSE)
33
-
34
- A robust Sphinx extension for creating accessible, JavaScript-free, filterable content tabs.
35
-
36
- **📖 View extension and documentation at: https://aputtu.github.io/sphinx-filter-tabs/**
37
-
38
- This extension provides `filter-tabs` and `tab` directives to create user-friendly, switchable content blocks, ideal for showing code examples in multiple languages or instructions for different platforms.
39
-
40
- ## Features
41
-
42
- - **No JavaScript:** Pure CSS implementation ensures maximum compatibility, speed, and accessibility.
43
- - **WAI-ARIA Compliant:** The generated HTML follows accessibility best practices for keyboard navigation and screen readers.
44
- - **Highly Customizable:** Easily theme colors, fonts, and sizes directly from your `conf.py` using CSS Custom Properties.
45
- - **Graceful Fallback:** Renders content as simple admonitions in non-HTML outputs like PDF/LaTeX.
46
- - **Automated Testing:** CI/CD pipeline tests against multiple Sphinx versions to ensure compatibility.
47
-
48
- ## Installation
49
-
50
- You can install this extension using `pip`:
51
- ```bash
52
- pip install sphinx-filter-tabs
53
- ```
54
-
55
- ## Development
56
-
57
- 1. You can install a local version of the Sphinx with extension using:
58
- ```bash
59
- ./scripts/setup_dev.sh # Initially cleans previous folders in _docs/build and venv.
60
- ```
61
-
62
- Command to enter venv is provided.
63
-
64
- 2. Once inside virtual environment, you can use following commands:
65
- ```bash
66
- pytest # Runs test suite on configured version of Sphinx.
67
- tox # Check across multiple Sphinx versions. Manual install of tox required.
68
- ./scripts/export-project.sh # Outputs directory structure and code to txt
69
- ./dev.sh [options] # Allows for faster generation for html, pdf, clean up
70
- ```
@@ -1,11 +0,0 @@
1
- filter_tabs/__init__.py,sha256=VPpIhj4HaLeMX7ai7dZFkUm81ii2ePPGjCd9hsMjsN4,397
2
- filter_tabs/extension.py,sha256=Dspt9r8TVoYChHFvGsncuV01GEga7BOOsPaMwYdMQcg,18014
3
- filter_tabs/renderer.py,sha256=m0_sD5ujtT4rmhYlhm6vMgSn0W1WAJPJWaTjh6zXezY,9730
4
- filter_tabs/static/filter_tabs.css,sha256=EbsG5zdEv8g3rdV7y76CXSwEMiBaBTX01qFYDiXeJt0,5379
5
- filter_tabs/static/filter_tabs.js,sha256=URduEo1P8y_-TaT485U6APGXsdQg6rFBpib3yaNc-9g,3989
6
- sphinx_filter_tabs-1.2.5.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
7
- sphinx_filter_tabs-1.2.5.dist-info/METADATA,sha256=nH86wxyNL4OAwRnPL0W8k4vJcj0mxKpoc_bk_2b2cv0,3483
8
- sphinx_filter_tabs-1.2.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
9
- sphinx_filter_tabs-1.2.5.dist-info/entry_points.txt,sha256=za_bQcueY8AHyq7XnnjkW9X3C-LsZjeERVQ_ds7jV1A,62
10
- sphinx_filter_tabs-1.2.5.dist-info/top_level.txt,sha256=K0Iy-6EsYYdvlyXdsJT0SQg-BLDBgT5-Y8ZKy5SNAfc,12
11
- sphinx_filter_tabs-1.2.5.dist-info/RECORD,,